api-server/src/worker.main.ts
wangdl bd60c2786f
Some checks failed
Deploy API Server / build-and-deploy (push) Has been cancelled
fix: correct require paths for dist/src output + systemd user
- 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>
2026-06-19 21:26:05 +08:00

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();