From 4043d69452ffe365ec1e6482ebde214858bdef29 Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Wed, 28 Jan 2026 17:05:00 +0000 Subject: [PATCH] fix(workers): skip background jobs when required tables are missing --- Backend/realtime/gateway.ts | 25 ++++++++++++++++++++----- Backend/services/push.ts | 24 +++++++++++++++++++----- Backend/utils/db-schema.ts | 24 ++++++++++++++++++++++++ Backend/workers/recordings.ts | 24 +++++++++++++++++++----- 4 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 Backend/utils/db-schema.ts diff --git a/Backend/realtime/gateway.ts b/Backend/realtime/gateway.ts index 5c11098..531f16f 100644 --- a/Backend/realtime/gateway.ts +++ b/Backend/realtime/gateway.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { db } from '../db/client'; import { deviceCommands, devices } from '../db/schema'; +import { hasRequiredTables } from '../utils/db-schema'; import { verifyDeviceToken } from '../utils/device-token'; const HEARTBEAT_INTERVAL_MS = 15_000; @@ -296,11 +297,25 @@ export const setupRealtimeGateway = (server: HttpServer): SocketIOServer => { }); if (!retryTimer) { - retryTimer = setInterval(() => { - retryPendingCommands().catch((error) => { - console.error('Failed retrying pending commands', error); - }); - }, RETRY_INTERVAL_MS); + const requiredTables = ['device_commands']; + + void (async () => { + const ready = await hasRequiredTables(requiredTables); + if (!ready) { + console.warn( + `[command retry] skipped startup because required tables are missing (${requiredTables.join(', ')}). Run migrations and restart.`, + ); + return; + } + + retryTimer = setInterval(() => { + retryPendingCommands().catch((error) => { + console.error('Failed retrying pending commands', error); + }); + }, RETRY_INTERVAL_MS); + })().catch((error) => { + console.error('Failed initializing command retry worker', error); + }); } return io; diff --git a/Backend/services/push.ts b/Backend/services/push.ts index 67f03e0..e2f6661 100644 --- a/Backend/services/push.ts +++ b/Backend/services/push.ts @@ -2,6 +2,7 @@ import { and, eq, lte } from 'drizzle-orm'; import { db } from '../db/client'; import { devices, pushNotifications } from '../db/schema'; +import { hasRequiredTables } from '../utils/db-schema'; const MAX_ATTEMPTS = Number(process.env.PUSH_MAX_ATTEMPTS ?? 5); @@ -83,10 +84,23 @@ export const dispatchPushQueueOnce = async (): Promise => { export const startPushWorker = (): void => { const intervalMs = Number(process.env.PUSH_WORKER_INTERVAL_MS ?? 10_000); + const requiredTables = ['push_notifications', 'devices']; - setInterval(() => { - dispatchPushQueueOnce().catch((error) => { - console.error('push worker failed', error); - }); - }, intervalMs); + void (async () => { + const ready = await hasRequiredTables(requiredTables); + if (!ready) { + console.warn( + `[push worker] skipped startup because required tables are missing (${requiredTables.join(', ')}). Run migrations and restart.`, + ); + return; + } + + setInterval(() => { + dispatchPushQueueOnce().catch((error) => { + console.error('push worker failed', error); + }); + }, intervalMs); + })().catch((error) => { + console.error('push worker failed to initialize', error); + }); }; diff --git a/Backend/utils/db-schema.ts b/Backend/utils/db-schema.ts new file mode 100644 index 0000000..de63659 --- /dev/null +++ b/Backend/utils/db-schema.ts @@ -0,0 +1,24 @@ +import { pool } from '../db/client'; + +const PUBLIC_SCHEMA = 'public'; + +export const tableExists = async (tableName: string): Promise => { + const result = await pool.query<{ exists: boolean }>( + ` + select exists ( + select 1 + from information_schema.tables + where table_schema = $1 + and table_name = $2 + ) as "exists" + `, + [PUBLIC_SCHEMA, tableName], + ); + + return result.rows[0]?.exists === true; +}; + +export const hasRequiredTables = async (tableNames: string[]): Promise => { + const checks = await Promise.all(tableNames.map((tableName) => tableExists(tableName))); + return checks.every(Boolean); +}; diff --git a/Backend/workers/recordings.ts b/Backend/workers/recordings.ts index a4abc3d..3ff84d2 100644 --- a/Backend/workers/recordings.ts +++ b/Backend/workers/recordings.ts @@ -2,17 +2,31 @@ import { and, eq, lt } from 'drizzle-orm'; import { db } from '../db/client'; import { recordings } from '../db/schema'; +import { hasRequiredTables } from '../utils/db-schema'; const STALE_RECORDING_SECONDS = Number(process.env.RECORDING_STALE_SECONDS ?? 60 * 30); export const startRecordingsWorker = (): void => { const intervalMs = Number(process.env.RECORDING_WORKER_INTERVAL_MS ?? 30_000); + const requiredTables = ['recordings']; - setInterval(() => { - reconcileStaleRecordings().catch((error) => { - console.error('recordings worker failed', error); - }); - }, intervalMs); + void (async () => { + const ready = await hasRequiredTables(requiredTables); + if (!ready) { + console.warn( + `[recordings worker] skipped startup because required tables are missing (${requiredTables.join(', ')}). Run migrations and restart.`, + ); + return; + } + + setInterval(() => { + reconcileStaleRecordings().catch((error) => { + console.error('recordings worker failed', error); + }); + }, intervalMs); + })().catch((error) => { + console.error('recordings worker failed to initialize', error); + }); }; const reconcileStaleRecordings = async (): Promise => {