initial commit
This commit is contained in:
69
apps/worker/src/health.ts
Normal file
69
apps/worker/src/health.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
|
||||
import { prisma } from '@hexahost/database';
|
||||
import { getConfig } from '@hexahost/config';
|
||||
|
||||
import { logger } from './logger';
|
||||
|
||||
export function startHealthServer(): ReturnType<typeof createServer> {
|
||||
const config = getConfig();
|
||||
|
||||
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.url !== '/health' && req.url !== '/health/ready') {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'error', message: 'Not found' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const isReady = req.url === '/health/ready';
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (!isReady) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: 'ok',
|
||||
service: config.APP_NAME,
|
||||
timestamp,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: 'ok',
|
||||
service: config.APP_NAME,
|
||||
timestamp,
|
||||
checks: [{ name: 'database', status: 'ok' }],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Readiness check failed');
|
||||
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: 'error',
|
||||
service: config.APP_NAME,
|
||||
timestamp,
|
||||
checks: [
|
||||
{
|
||||
name: 'database',
|
||||
status: 'error',
|
||||
message: error instanceof Error ? error.message : 'Database check failed',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(config.WORKER_PORT, '0.0.0.0', () => {
|
||||
logger.info({ port: config.WORKER_PORT }, 'Worker health server listening');
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
80
apps/worker/src/index.ts
Normal file
80
apps/worker/src/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Worker, type ConnectionOptions } from 'bullmq';
|
||||
import IORedis from 'ioredis';
|
||||
|
||||
import { validateConfig } from '@hexahost/config';
|
||||
import { prisma } from '@hexahost/database';
|
||||
|
||||
import { startHealthServer } from './health';
|
||||
import { logger } from './logger';
|
||||
import { WORKER_QUEUES, type WorkerQueueName } from './queues';
|
||||
|
||||
function createRedisConnection(redisUrl: string): IORedis {
|
||||
return new IORedis(redisUrl, {
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
}
|
||||
|
||||
function createQueueWorker(
|
||||
queueName: WorkerQueueName,
|
||||
connection: ConnectionOptions,
|
||||
): Worker {
|
||||
return new Worker(
|
||||
queueName,
|
||||
async (job) => {
|
||||
logger.info(
|
||||
{ queue: queueName, jobId: job.id, jobName: job.name },
|
||||
'Processing job',
|
||||
);
|
||||
|
||||
return { acknowledged: true, queue: queueName };
|
||||
},
|
||||
{ connection },
|
||||
);
|
||||
}
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const config = validateConfig();
|
||||
|
||||
const healthServer = startHealthServer();
|
||||
const redis = createRedisConnection(config.REDIS_URL);
|
||||
const workers = WORKER_QUEUES.map((queueName) =>
|
||||
createQueueWorker(queueName, redis),
|
||||
);
|
||||
|
||||
for (const worker of workers) {
|
||||
worker.on('completed', (job) => {
|
||||
logger.info({ queue: worker.name, jobId: job.id }, 'Job completed');
|
||||
});
|
||||
|
||||
worker.on('failed', (job, error) => {
|
||||
logger.error(
|
||||
{ queue: worker.name, jobId: job?.id, err: error },
|
||||
'Job failed',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({ queues: WORKER_QUEUES }, 'Worker started');
|
||||
|
||||
const shutdown = async (signal: string): Promise<void> => {
|
||||
logger.info({ signal }, 'Shutting down worker');
|
||||
|
||||
await Promise.all(workers.map((worker) => worker.close()));
|
||||
await redis.quit();
|
||||
await prisma.$disconnect();
|
||||
|
||||
healthServer.close(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
void shutdown('SIGINT');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
void shutdown('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
10
apps/worker/src/logger.ts
Normal file
10
apps/worker/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import pino from 'pino';
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env['LOG_LEVEL'] ?? 'info',
|
||||
base: {
|
||||
service: process.env['APP_NAME'] ?? 'HexaHost GameCloud Worker',
|
||||
env: process.env['APP_ENV'] ?? 'development',
|
||||
},
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
});
|
||||
14
apps/worker/src/main.ts
Normal file
14
apps/worker/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const redisUrl = process.env.REDIS_URL ?? "redis://localhost:6379";
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
level: "info",
|
||||
msg: "worker started (phase 0 stub)",
|
||||
redis: redisUrl.replace(/:[^:@]+@/, ":***@"),
|
||||
}),
|
||||
);
|
||||
|
||||
// Phase 0: worker process stays alive for compose; BullMQ queues land in Phase 1+
|
||||
setInterval(() => {
|
||||
console.log(JSON.stringify({ level: "debug", msg: "worker heartbeat" }));
|
||||
}, 60_000);
|
||||
11
apps/worker/src/queues.ts
Normal file
11
apps/worker/src/queues.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const QUEUE_SERVER_LIFECYCLE = 'server-lifecycle' as const;
|
||||
export const QUEUE_SERVER_PROVISIONING = 'server-provisioning' as const;
|
||||
export const QUEUE_NOTIFICATIONS = 'notifications' as const;
|
||||
|
||||
export const WORKER_QUEUES = [
|
||||
QUEUE_SERVER_LIFECYCLE,
|
||||
QUEUE_SERVER_PROVISIONING,
|
||||
QUEUE_NOTIFICATIONS,
|
||||
] as const;
|
||||
|
||||
export type WorkerQueueName = (typeof WORKER_QUEUES)[number];
|
||||
Reference in New Issue
Block a user