import { NestFactory } from '@nestjs/core'; import { WorkerModule } from './worker.module'; import { ConfigService } from '@nestjs/config'; import { PrismaService } from './infrastructure/database/prisma.service'; import { RedisService } from './infrastructure/redis/redis.service'; import { Logger } from '@nestjs/common'; import * as os from 'os'; function generateInstanceId(): string { const hostname = os.hostname(); const pid = process.pid; const timestamp = Date.now(); return `worker-${hostname}-${pid}-${timestamp}`; } async function validatePrerequisites( prisma: PrismaService, redis: RedisService, config: ConfigService, logger: Logger, ): Promise { // Validate MySQL try { await prisma.$queryRaw`SELECT 1`; logger.log('MySQL connection verified'); } catch (err: any) { logger.error(`MySQL connection failed: ${err.message}`); process.exit(1); } // Validate Redis try { await redis.get('__worker_health_check__'); logger.log('Redis connection verified'); } catch (err: any) { logger.error(`Redis connection failed: ${err.message}`); process.exit(1); } // Validate required config keys const requiredKeys = ['database.url', 'redis.host', 'jwt.secret']; const missing = requiredKeys.filter(k => { const val = config.get(k); if (val === undefined || val === null || val === '') return true; if (typeof val === 'string' && val.startsWith('change_me')) return true; return false; }); if (missing.length > 0) { logger.error(`Missing required config keys: ${missing.join(', ')}`); process.exit(1); } logger.log('Required configuration verified'); } async function bootstrap() { const logger = new Logger('WorkerBootstrap'); // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = (() => { try { return require('../../package.json'); } catch { return { version: '0.0.0' }; } })(); const instanceId = generateInstanceId(); process.env.WORKER_INSTANCE_ID = instanceId; logger.log(`Worker version ${pkg.version} — instance ${instanceId}`); const app = await NestFactory.createApplicationContext(WorkerModule); const config = app.get(ConfigService); const prisma = app.get(PrismaService); const redis = app.get(RedisService); await validatePrerequisites(prisma, redis, config, logger); // Log queue configuration try { const { QUEUE_DEFINITIONS } = require('./infrastructure/queue/queue-definitions'); logger.log('Queue configuration:'); for (const [name, def] of Object.entries(QUEUE_DEFINITIONS) as [string, any][]) { logger.log( ` ${name}: concurrency=${def.workerOptions.concurrency} attempts=${def.defaultJobOptions.attempts} backoff=exponential:${def.defaultJobOptions.backoff.delay}ms`, ); } } catch { logger.warn('Could not load queue definitions for startup log'); } app.enableShutdownHooks(); logger.log(`Worker ${instanceId} started — waiting for jobs`); // Graceful shutdown with forced timeout const SHUTDOWN_TIMEOUT_MS = 30_000; let shuttingDown = false; const shutdown = (signal: string) => { if (shuttingDown) return; shuttingDown = true; logger.log(`Received ${signal} — stopping job acquisition, waiting for active jobs (timeout ${SHUTDOWN_TIMEOUT_MS / 1000}s)...`); const forceExit = setTimeout(() => { logger.error(`Shutdown timed out after ${SHUTDOWN_TIMEOUT_MS / 1000}s — forcing exit`); process.exit(1); }, SHUTDOWN_TIMEOUT_MS); app.close().then(() => { clearTimeout(forceExit); logger.log('Worker shut down gracefully'); process.exit(0); }).catch((err: any) => { clearTimeout(forceExit); logger.error(`Worker shutdown error: ${err.message}`); process.exit(1); }); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } bootstrap();