import cron from "node-cron"; import dotenv from "dotenv"; import pg from "pg"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import kafka from "kafka-node"; import { randomUUID } from "crypto"; import { z } from "zod"; import { createClient } from "redis"; dotenv.config(); const parseNumber = (value, defaultValue) => { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : defaultValue; }; const parseList = (value) => (value || "").split(",").map((item) => item.trim()).filter(Boolean); const config = { env: process.env.NODE_ENV || "development", port: parseNumber(process.env.PORT, 3001), kafka: { brokers: parseList(process.env.KAFKA_BROKERS), topic: process.env.KAFKA_TOPIC || process.env.KAFKA_TOPICS || "blwlog4Nodejs-rcu-onoffline-topic", groupId: process.env.KAFKA_GROUP_ID || "bls-onoffline-group", clientId: process.env.KAFKA_CLIENT_ID || "bls-onoffline-client", consumerInstances: parseNumber(process.env.KAFKA_CONSUMER_INSTANCES, 1), maxInFlight: parseNumber(process.env.KAFKA_MAX_IN_FLIGHT, 50), fetchMaxBytes: parseNumber(process.env.KAFKA_FETCH_MAX_BYTES, 10 * 1024 * 1024), fetchMinBytes: parseNumber(process.env.KAFKA_FETCH_MIN_BYTES, 1), fetchMaxWaitMs: parseNumber(process.env.KAFKA_FETCH_MAX_WAIT_MS, 100), autoCommitIntervalMs: parseNumber(process.env.KAFKA_AUTO_COMMIT_INTERVAL_MS, 5e3), logMessages: process.env.KAFKA_LOG_MESSAGES === "true", sasl: process.env.KAFKA_SASL_USERNAME && process.env.KAFKA_SASL_PASSWORD ? { mechanism: process.env.KAFKA_SASL_MECHANISM || "plain", username: process.env.KAFKA_SASL_USERNAME, password: process.env.KAFKA_SASL_PASSWORD } : void 0 }, db: { host: process.env.DB_HOST || process.env.POSTGRES_HOST || "localhost", port: parseNumber(process.env.DB_PORT || process.env.POSTGRES_PORT, 5432), user: process.env.DB_USER || process.env.POSTGRES_USER || "postgres", password: process.env.DB_PASSWORD || process.env.POSTGRES_PASSWORD || "", database: process.env.DB_DATABASE || process.env.POSTGRES_DATABASE || "log_platform", max: parseNumber(process.env.DB_MAX_CONNECTIONS || process.env.POSTGRES_MAX_CONNECTIONS, 10), ssl: process.env.DB_SSL === "true" ? { rejectUnauthorized: false } : void 0, schema: process.env.DB_SCHEMA || "onoffline", table: process.env.DB_TABLE || "onoffline_record" }, redis: { host: process.env.REDIS_HOST || "localhost", port: parseNumber(process.env.REDIS_PORT, 6379), password: process.env.REDIS_PASSWORD || void 0, db: parseNumber(process.env.REDIS_DB, 0), projectName: process.env.REDIS_PROJECT_NAME || "bls-onoffline", apiBaseUrl: process.env.REDIS_API_BASE_URL || `http://localhost:${parseNumber(process.env.PORT, 3001)}` } }; const format = (level, message, context) => { const payload = { level, message, timestamp: Date.now(), ...context ? { context } : {} }; return JSON.stringify(payload); }; const logger$1 = { info(message, context) { process.stdout.write(`${format("info", message, context)} `); }, error(message, context) { process.stderr.write(`${format("error", message, context)} `); } }; const { Pool } = pg; const columns = [ "guid", "ts_ms", "write_ts_ms", "hotel_id", "mac", "device_id", "room_id", "ip", "current_status", "launcher_version", "reboot_reason" ]; class DatabaseManager { constructor(dbConfig) { this.pool = new Pool({ host: dbConfig.host, port: dbConfig.port, user: dbConfig.user, password: dbConfig.password, database: dbConfig.database, max: dbConfig.max, ssl: dbConfig.ssl }); } async insertRows({ schema, table, rows }) { if (!rows || rows.length === 0) { return; } const values = []; const placeholders = rows.map((row, rowIndex) => { const offset = rowIndex * columns.length; columns.forEach((column) => { values.push(row[column] ?? null); }); const params = columns.map((_, columnIndex) => `$${offset + columnIndex + 1}`); return `(${params.join(", ")})`; }); const statement = ` INSERT INTO ${schema}.${table} (${columns.join(", ")}) VALUES ${placeholders.join(", ")} ON CONFLICT DO NOTHING `; try { await this.pool.query(statement, values); } catch (error) { logger$1.error("Database insert failed", { error: error?.message, schema, table, rowsLength: rows.length }); throw error; } } async checkConnection() { let client; try { const connectPromise = this.pool.connect(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("Connection timeout")), 5e3); }); try { client = await Promise.race([connectPromise, timeoutPromise]); } catch (raceError) { connectPromise.then((c) => c.release()).catch(() => { }); throw raceError; } await client.query("SELECT 1"); return true; } catch (err) { logger$1.error("Database check connection failed", { error: err.message }); return false; } finally { if (client) { client.release(); } } } async close() { await this.pool.end(); } } const dbManager = new DatabaseManager(config.db); class PartitionManager { /** * Calculate the start and end timestamps (milliseconds) for a given date. * @param {Date} date - The date to calculate for. * @returns {Object} { startMs, endMs, partitionSuffix } */ getPartitionInfo(date) { const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); const partitionSuffix = `${yyyy}${mm}${dd}`; const start = new Date(date); start.setHours(0, 0, 0, 0); const startMs = start.getTime(); const end = new Date(date); end.setDate(end.getDate() + 1); end.setHours(0, 0, 0, 0); const endMs = end.getTime(); return { startMs, endMs, partitionSuffix }; } /** * Ensure partitions exist for the past M days and next N days. * @param {number} daysAhead - Number of days to pre-create. * @param {number} daysBack - Number of days to look back. */ async ensurePartitions(daysAhead = 30, daysBack = 15) { const client = await dbManager.pool.connect(); try { logger$1.info(`Starting partition check for the past ${daysBack} days and next ${daysAhead} days...`); console.log(`Starting partition check for the past ${daysBack} days and next ${daysAhead} days...`); const now = /* @__PURE__ */ new Date(); for (let i = -daysBack; i < daysAhead; i++) { const targetDate = new Date(now); targetDate.setDate(now.getDate() + i); const { startMs, endMs, partitionSuffix } = this.getPartitionInfo(targetDate); const schema = config.db.schema; const table = config.db.table; const partitionName = `${schema}.${table}_${partitionSuffix}`; const checkSql = ` SELECT to_regclass($1) as exists; `; const checkRes = await client.query(checkSql, [partitionName]); if (!checkRes.rows[0].exists) { logger$1.info(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); console.log(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); const createSql = ` CREATE TABLE IF NOT EXISTS ${partitionName} PARTITION OF ${schema}.${table} FOR VALUES FROM (${startMs}) TO (${endMs}); `; await client.query(createSql); } } logger$1.info("Partition check completed."); } catch (err) { logger$1.error("Error ensuring partitions:", err); throw err; } finally { client.release(); } } } const partitionManager = new PartitionManager(); const __filename$1 = fileURLToPath(import.meta.url); const __dirname$1 = path.dirname(__filename$1); class DatabaseInitializer { async initialize() { logger$1.info("Starting database initialization check..."); await this.ensureDatabaseExists(); await this.ensureSchemaAndTable(); await partitionManager.ensurePartitions(30); console.log("Database initialization completed successfully."); logger$1.info("Database initialization completed successfully."); } async ensureDatabaseExists() { const { host, port, user, password, database, ssl } = config.db; console.log(`Checking if database '${database}' exists at ${host}:${port}...`); const client = new pg.Client({ host, port, user, password, database: "postgres", ssl: ssl ? { rejectUnauthorized: false } : false }); try { await client.connect(); const checkRes = await client.query( `SELECT 1 FROM pg_database WHERE datname = $1`, [database] ); if (checkRes.rowCount === 0) { logger$1.info(`Database '${database}' does not exist. Creating...`); await client.query(`CREATE DATABASE "${database}"`); console.log(`Database '${database}' created.`); logger$1.info(`Database '${database}' created.`); } else { console.log(`Database '${database}' already exists.`); logger$1.info(`Database '${database}' already exists.`); } } catch (err) { logger$1.error("Error ensuring database exists:", err); throw err; } finally { await client.end(); } } async ensureSchemaAndTable() { const client = await dbManager.pool.connect(); try { const sqlPathCandidates = [ path.resolve(process.cwd(), "scripts/init_db.sql"), path.resolve(__dirname$1, "../scripts/init_db.sql"), path.resolve(__dirname$1, "../../scripts/init_db.sql") ]; const sqlPath = sqlPathCandidates.find((candidate) => fs.existsSync(candidate)); if (!sqlPath) { throw new Error(`init_db.sql not found. Candidates: ${sqlPathCandidates.join(" | ")}`); } const sql = fs.readFileSync(sqlPath, "utf8"); console.log(`Executing init_db.sql from ${sqlPath}...`); logger$1.info("Executing init_db.sql..."); await client.query(sql); console.log("Schema and parent table initialized."); logger$1.info("Schema and parent table initialized."); } catch (err) { logger$1.error("Error initializing schema and table:", err); throw err; } finally { client.release(); } } } const dbInitializer = new DatabaseInitializer(); class OffsetTracker { constructor() { this.partitions = /* @__PURE__ */ new Map(); } // Called when a message is received (before processing) add(topic, partition, offset) { const key = `${topic}-${partition}`; if (!this.partitions.has(key)) { this.partitions.set(key, []); } this.partitions.get(key).push({ offset, done: false }); } // Called when a message is successfully processed // Returns the next offset to commit (if any advancement is possible), or null markDone(topic, partition, offset) { const key = `${topic}-${partition}`; const list = this.partitions.get(key); if (!list) return null; const item = list.find((i) => i.offset === offset); if (item) { item.done = true; } let lastDoneOffset = null; let itemsRemoved = false; while (list.length > 0 && list[0].done) { lastDoneOffset = list[0].offset; list.shift(); itemsRemoved = true; } if (itemsRemoved && lastDoneOffset !== null) { return lastDoneOffset + 1; } return null; } } const { ConsumerGroup } = kafka; const createOneConsumer = ({ kafkaConfig, onMessage, onError, instanceIndex }) => { const kafkaHost = kafkaConfig.brokers.join(","); const clientId = instanceIndex === 0 ? kafkaConfig.clientId : `${kafkaConfig.clientId}-${instanceIndex}`; const id = `${clientId}-${process.pid}-${Date.now()}`; const maxInFlight = Number.isFinite(kafkaConfig.maxInFlight) ? kafkaConfig.maxInFlight : 50; let inFlight = 0; const tracker = new OffsetTracker(); const consumer = new ConsumerGroup( { kafkaHost, groupId: kafkaConfig.groupId, clientId, id, fromOffset: "earliest", protocol: ["roundrobin"], outOfRangeOffset: "latest", autoCommit: false, autoCommitIntervalMs: kafkaConfig.autoCommitIntervalMs, fetchMaxBytes: kafkaConfig.fetchMaxBytes, fetchMinBytes: kafkaConfig.fetchMinBytes, fetchMaxWaitMs: kafkaConfig.fetchMaxWaitMs, sasl: kafkaConfig.sasl }, kafkaConfig.topic ); const tryResume = () => { if (inFlight < maxInFlight && consumer.paused) { consumer.resume(); } }; consumer.on("message", (message) => { inFlight += 1; tracker.add(message.topic, message.partition, message.offset); if (inFlight >= maxInFlight) { consumer.pause(); } Promise.resolve(onMessage(message)).then(() => { const commitOffset = tracker.markDone(message.topic, message.partition, message.offset); if (commitOffset !== null) { consumer.sendOffsetCommitRequest([{ topic: message.topic, partition: message.partition, offset: commitOffset, metadata: "m" }], (err) => { if (err) { logger$1.error("Kafka commit failed", { error: err?.message, topic: message.topic, partition: message.partition, offset: commitOffset }); } }); } }).catch((error) => { logger$1.error("Kafka message handling failed", { error: error?.message }); if (onError) { onError(error, message); } }).finally(() => { inFlight -= 1; tryResume(); }); }); consumer.on("error", (error) => { logger$1.error("Kafka consumer error", { error: error?.message }); if (onError) { onError(error); } }); consumer.on("connect", () => { logger$1.info(`Kafka Consumer connected`, { groupId: kafkaConfig.groupId, clientId }); }); consumer.on("rebalancing", () => { logger$1.info(`Kafka Consumer rebalancing`, { groupId: kafkaConfig.groupId, clientId }); }); consumer.on("rebalanced", () => { logger$1.info("Kafka Consumer rebalanced", { clientId, groupId: kafkaConfig.groupId }); }); consumer.on("error", (err) => { logger$1.error("Kafka Consumer Error", { error: err.message }); }); consumer.on("offsetOutOfRange", (err) => { logger$1.warn("Offset out of range", { error: err.message, topic: err.topic, partition: err.partition }); }); consumer.on("offsetOutOfRange", (error) => { logger$1.warn(`Kafka Consumer offset out of range`, { error: error?.message, groupId: kafkaConfig.groupId, clientId }); }); consumer.on("close", () => { logger$1.warn(`Kafka Consumer closed`, { groupId: kafkaConfig.groupId, clientId }); }); return consumer; }; const createKafkaConsumers = ({ kafkaConfig, onMessage, onError }) => { const instances = Number.isFinite(kafkaConfig.consumerInstances) ? kafkaConfig.consumerInstances : 1; const count = Math.max(1, instances); return Array.from( { length: count }, (_, idx) => createOneConsumer({ kafkaConfig, onMessage, onError, instanceIndex: idx }) ); }; const createGuid = () => randomUUID().replace(/-/g, ""); const toNumber = (value) => { if (value === void 0 || value === null || value === "") { return value; } if (typeof value === "number") { return value; } const parsed = Number(value); return Number.isFinite(parsed) ? parsed : value; }; const toStringAllowEmpty = (value) => { if (value === void 0 || value === null) { return value; } return String(value); }; const kafkaPayloadSchema = z.object({ HotelCode: z.preprocess(toNumber, z.number()), MAC: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), HostNumber: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), RoomNumber: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), EndPoint: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), CurrentStatus: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), CurrentTime: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), UnixTime: z.preprocess(toNumber, z.number().nullable()).optional().nullable(), LauncherVersion: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable(), RebootReason: z.preprocess(toStringAllowEmpty, z.string().nullable()).optional().nullable() }); const normalizeText = (value, maxLength) => { if (value === void 0 || value === null) { return null; } const str = String(value); if (maxLength && str.length > maxLength) { return str.substring(0, maxLength); } return str; }; const buildRowsFromPayload = (rawPayload) => { const payload = kafkaPayloadSchema.parse(rawPayload); const rebootReason = normalizeText(payload.RebootReason, 255); const currentStatusRaw = normalizeText(payload.CurrentStatus, 255); const hasRebootReason = rebootReason !== null && rebootReason !== ""; const currentStatus = hasRebootReason ? "on" : currentStatusRaw; let tsMs = payload.UnixTime; if (typeof tsMs === "number" && tsMs < 1e11) { tsMs = tsMs * 1e3; } if (!tsMs && payload.CurrentTime) { const parsed = Date.parse(payload.CurrentTime); if (!isNaN(parsed)) { tsMs = parsed; } } if (!tsMs) { tsMs = Date.now(); } const mac = normalizeText(payload.MAC) || ""; const deviceId = normalizeText(payload.HostNumber) || ""; const roomId = normalizeText(payload.RoomNumber) || ""; const row = { guid: createGuid(), ts_ms: tsMs, write_ts_ms: Date.now(), hotel_id: payload.HotelCode, mac, device_id: deviceId, room_id: roomId, ip: normalizeText(payload.EndPoint), current_status: currentStatus, launcher_version: normalizeText(payload.LauncherVersion, 255), reboot_reason: rebootReason }; return [row]; }; const processKafkaMessage = async ({ message, dbManager: dbManager2, config: config2 }) => { let rows; try { const rawValue = message.value.toString(); let payload; try { payload = JSON.parse(rawValue); } catch (e) { logger.error("JSON Parse Error", { error: e.message, rawValue }); const error = new Error(`JSON Parse Error: ${e.message}`); error.type = "PARSE_ERROR"; throw error; } const validationResult = kafkaPayloadSchema.safeParse(payload); if (!validationResult.success) { logger.error("Schema Validation Failed", { errors: validationResult.error.errors, payload }); const error = new Error(`Schema Validation Failed: ${JSON.stringify(validationResult.error.errors)}`); error.type = "VALIDATION_ERROR"; throw error; } rows = buildRowsFromPayload(payload); } catch (error) { throw error; } try { await dbManager2.insertRows({ schema: config2.db.schema, table: config2.db.table, rows }); } catch (error) { error.type = "DB_ERROR"; const sample = rows?.[0]; error.dbContext = { rowsLength: rows?.length || 0, sampleRow: sample ? { guid: sample.guid, ts_ms: sample.ts_ms, mac: sample.mac, device_id: sample.device_id, room_id: sample.room_id, current_status: sample.current_status } : null }; throw error; } return rows.length; }; const createRedisClient = async (config2) => { const client = createClient({ socket: { host: config2.host, port: config2.port }, password: config2.password, database: config2.db }); await client.connect(); return client; }; class RedisIntegration { constructor(client, projectName, apiBaseUrl) { this.client = client; this.projectName = projectName; this.apiBaseUrl = apiBaseUrl; this.heartbeatKey = "项目心跳"; this.logKey = `${projectName}_项目控制台`; } async info(message, context) { const payload = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", message, metadata: context || void 0 }; await this.client.rPush(this.logKey, JSON.stringify(payload)); } async error(message, context) { const payload = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "error", message, metadata: context || void 0 }; await this.client.rPush(this.logKey, JSON.stringify(payload)); } startHeartbeat() { setInterval(() => { const payload = { projectName: this.projectName, apiBaseUrl: this.apiBaseUrl, lastActiveAt: Date.now() }; this.client.rPush(this.heartbeatKey, JSON.stringify(payload)); }, 3e3); } } const buildErrorQueueKey = (projectName) => `${projectName}_error_queue`; const enqueueError = async (client, queueKey, payload) => { try { await client.rPush(queueKey, JSON.stringify(payload)); } catch (error) { logger$1.error("Redis enqueue error failed", { error: error?.message }); throw error; } }; const startErrorRetryWorker = async ({ client, queueKey, handler, redisIntegration, maxAttempts = 5 }) => { while (true) { const result = await client.blPop(queueKey, 0); const raw = result?.element; if (!raw) { continue; } let item; try { item = JSON.parse(raw); } catch (error) { logger$1.error("Invalid error payload", { error: error?.message }); await redisIntegration.error("Invalid error payload", { module: "redis", stack: error?.message }); continue; } const attempts = item.attempts || 0; try { await handler(item); } catch (error) { logger$1.error("Retry handler failed", { error: error?.message, stack: error?.stack }); const nextPayload = { ...item, attempts: attempts + 1, lastError: error?.message, lastAttemptAt: Date.now() }; if (nextPayload.attempts >= maxAttempts) { await redisIntegration.error("Retry attempts exceeded", { module: "retry", stack: JSON.stringify(nextPayload) }); } else { await enqueueError(client, queueKey, nextPayload); } } } }; class MetricCollector { constructor() { this.reset(); } reset() { this.metrics = { kafka_pulled: 0, parse_error: 0, db_inserted: 0, db_failed: 0 }; } increment(metric, count = 1) { if (this.metrics.hasOwnProperty(metric)) { this.metrics[metric] += count; } } getAndReset() { const current = { ...this.metrics }; this.reset(); return current; } } const bootstrap = async () => { logger$1.info("Starting application with config", { env: process.env.NODE_ENV, db: { host: config.db.host, port: config.db.port, user: config.db.user, database: config.db.database, schema: config.db.schema }, kafka: { brokers: config.kafka.brokers, topic: config.kafka.topic, groupId: config.kafka.groupId }, redis: { host: config.redis.host, port: config.redis.port } }); await dbInitializer.initialize(); const metricCollector = new MetricCollector(); cron.schedule("0 0 * * *", async () => { logger$1.info("Running scheduled partition maintenance..."); try { await partitionManager.ensurePartitions(30); } catch (err) { logger$1.error("Scheduled partition maintenance failed", err); } }); const redisClient = await createRedisClient(config.redis); const redisIntegration = new RedisIntegration( redisClient, config.redis.projectName, config.redis.apiBaseUrl ); redisIntegration.startHeartbeat(); cron.schedule("* * * * *", async () => { const metrics = metricCollector.getAndReset(); const report = `[Minute Metrics] Pulled: ${metrics.kafka_pulled}, Parse Error: ${metrics.parse_error}, Inserted: ${metrics.db_inserted}, Failed: ${metrics.db_failed}`; console.log(report); logger$1.info(report, metrics); try { await redisIntegration.info("Minute Metrics", metrics); } catch (err) { logger$1.error("Failed to report metrics to Redis", { error: err?.message }); } }); const errorQueueKey = buildErrorQueueKey(config.redis.projectName); const handleError = async (error, message) => { logger$1.error("Kafka processing error", { error: error?.message, type: error?.type, stack: error?.stack }); try { await redisIntegration.error("Kafka processing error", { module: "kafka", stack: error?.stack || error?.message }); } catch (redisError) { logger$1.error("Redis error log failed", { error: redisError?.message }); } if (message) { const messageValue = Buffer.isBuffer(message.value) ? message.value.toString("utf8") : message.value; try { await enqueueError(redisClient, errorQueueKey, { attempts: 0, value: messageValue, meta: { topic: message.topic, partition: message.partition, offset: message.offset, key: message.key }, timestamp: Date.now() }); } catch (enqueueError2) { logger$1.error("Enqueue error payload failed", { error: enqueueError2?.message }); } } }; const handleMessage = async (message) => { if (message.topic) { metricCollector.increment("kafka_pulled"); } const messageValue = Buffer.isBuffer(message.value) ? message.value.toString("utf8") : message.value; Buffer.isBuffer(message.key) ? message.key.toString("utf8") : message.key; ({ topic: message.topic, partition: message.partition, offset: message.offset, valueLength: !config.kafka.logMessages && typeof messageValue === "string" ? messageValue.length : null }); while (true) { try { const inserted = await processKafkaMessage({ message, dbManager, config }); metricCollector.increment("db_inserted"); return; } catch (error) { const isDbConnectionError = error.code && ["ECONNREFUSED", "57P03", "08006", "08001", "EADDRINUSE", "ETIMEDOUT"].includes(error.code) || error.message && (error.message.includes("ECONNREFUSED") || error.message.includes("connection") || error.message.includes("terminated") || error.message.includes("EADDRINUSE") || error.message.includes("ETIMEDOUT") || error.message.includes("The server does not support SSL connections")); if (isDbConnectionError) { logger$1.error("Database offline. Pausing consumption for 1 minute...", { error: error.message }); await new Promise((resolve) => setTimeout(resolve, 6e4)); while (true) { const isConnected = await dbManager.checkConnection(); if (isConnected) { logger$1.info("Database connection restored. Resuming processing..."); break; } logger$1.warn("Database still offline. Waiting 1 minute..."); await new Promise((resolve) => setTimeout(resolve, 6e4)); } } else { if (error.type === "PARSE_ERROR") { metricCollector.increment("parse_error"); } else { metricCollector.increment("db_failed"); } logger$1.error("Message processing failed (Data/Logic Error), skipping message", { error: error?.message, type: error?.type }); await handleError(error, message); return; } } } }; const consumers = createKafkaConsumers({ kafkaConfig: config.kafka, onMessage: handleMessage, onError: handleError }); startErrorRetryWorker({ client: redisClient, queueKey: errorQueueKey, redisIntegration, handler: async (item) => { if (!item?.value) { throw new Error("Missing value in retry payload"); } await handleMessage({ value: item.value }); } }).catch((err) => { logger$1.error("Retry worker failed", { error: err?.message }); }); const shutdown = async (signal) => { logger$1.info(`Received ${signal}, shutting down...`); try { if (consumers && consumers.length > 0) { await Promise.all(consumers.map((c) => new Promise((resolve) => c.close(true, resolve)))); logger$1.info("Kafka consumer closed", { count: consumers.length }); } await redisClient.quit(); logger$1.info("Redis client closed"); await dbManager.close(); logger$1.info("Database connection closed"); process.exit(0); } catch (err) { logger$1.error("Error during shutdown", { error: err?.message }); process.exit(1); } }; process.on("SIGTERM", () => shutdown("SIGTERM")); process.on("SIGINT", () => shutdown("SIGINT")); }; bootstrap().catch((error) => { logger$1.error("Service bootstrap failed", { error: error?.message }); process.exit(1); });