Some checks failed
Deploy API Server / build-and-deploy (push) Has been cancelled
- worker.main.ts: require('../../package.json') for dist/src/ location
- worker-heartbeat.service.ts: require('../../../package.json') + try/catch
- systemd units: User=ubuntu (www-data not present on production)
- EnvironmentFile: /opt/zhixi/env/.env.production
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
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<void> {
|
|
// 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();
|