fix(workers): skip background jobs when required tables are missing

This commit is contained in:
2026-01-28 17:05:00 +00:00
parent 83d7e1a465
commit 4043d69452
4 changed files with 82 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ import { z } from 'zod';
import { db } from '../db/client'; import { db } from '../db/client';
import { deviceCommands, devices } from '../db/schema'; import { deviceCommands, devices } from '../db/schema';
import { hasRequiredTables } from '../utils/db-schema';
import { verifyDeviceToken } from '../utils/device-token'; import { verifyDeviceToken } from '../utils/device-token';
const HEARTBEAT_INTERVAL_MS = 15_000; const HEARTBEAT_INTERVAL_MS = 15_000;
@@ -296,11 +297,25 @@ export const setupRealtimeGateway = (server: HttpServer): SocketIOServer => {
}); });
if (!retryTimer) { if (!retryTimer) {
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(() => { retryTimer = setInterval(() => {
retryPendingCommands().catch((error) => { retryPendingCommands().catch((error) => {
console.error('Failed retrying pending commands', error); console.error('Failed retrying pending commands', error);
}); });
}, RETRY_INTERVAL_MS); }, RETRY_INTERVAL_MS);
})().catch((error) => {
console.error('Failed initializing command retry worker', error);
});
} }
return io; return io;

View File

@@ -2,6 +2,7 @@ import { and, eq, lte } from 'drizzle-orm';
import { db } from '../db/client'; import { db } from '../db/client';
import { devices, pushNotifications } from '../db/schema'; import { devices, pushNotifications } from '../db/schema';
import { hasRequiredTables } from '../utils/db-schema';
const MAX_ATTEMPTS = Number(process.env.PUSH_MAX_ATTEMPTS ?? 5); const MAX_ATTEMPTS = Number(process.env.PUSH_MAX_ATTEMPTS ?? 5);
@@ -83,10 +84,23 @@ export const dispatchPushQueueOnce = async (): Promise<number> => {
export const startPushWorker = (): void => { export const startPushWorker = (): void => {
const intervalMs = Number(process.env.PUSH_WORKER_INTERVAL_MS ?? 10_000); const intervalMs = Number(process.env.PUSH_WORKER_INTERVAL_MS ?? 10_000);
const requiredTables = ['push_notifications', 'devices'];
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(() => { setInterval(() => {
dispatchPushQueueOnce().catch((error) => { dispatchPushQueueOnce().catch((error) => {
console.error('push worker failed', error); console.error('push worker failed', error);
}); });
}, intervalMs); }, intervalMs);
})().catch((error) => {
console.error('push worker failed to initialize', error);
});
}; };

View File

@@ -0,0 +1,24 @@
import { pool } from '../db/client';
const PUBLIC_SCHEMA = 'public';
export const tableExists = async (tableName: string): Promise<boolean> => {
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<boolean> => {
const checks = await Promise.all(tableNames.map((tableName) => tableExists(tableName)));
return checks.every(Boolean);
};

View File

@@ -2,17 +2,31 @@ import { and, eq, lt } from 'drizzle-orm';
import { db } from '../db/client'; import { db } from '../db/client';
import { recordings } from '../db/schema'; import { recordings } from '../db/schema';
import { hasRequiredTables } from '../utils/db-schema';
const STALE_RECORDING_SECONDS = Number(process.env.RECORDING_STALE_SECONDS ?? 60 * 30); const STALE_RECORDING_SECONDS = Number(process.env.RECORDING_STALE_SECONDS ?? 60 * 30);
export const startRecordingsWorker = (): void => { export const startRecordingsWorker = (): void => {
const intervalMs = Number(process.env.RECORDING_WORKER_INTERVAL_MS ?? 30_000); const intervalMs = Number(process.env.RECORDING_WORKER_INTERVAL_MS ?? 30_000);
const requiredTables = ['recordings'];
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(() => { setInterval(() => {
reconcileStaleRecordings().catch((error) => { reconcileStaleRecordings().catch((error) => {
console.error('recordings worker failed', error); console.error('recordings worker failed', error);
}); });
}, intervalMs); }, intervalMs);
})().catch((error) => {
console.error('recordings worker failed to initialize', error);
});
}; };
const reconcileStaleRecordings = async (): Promise<void> => { const reconcileStaleRecordings = async (): Promise<void> => {