import { Kafka, type Consumer, type Producer } from "kafkajs"; import { env } from "../../config/env.js"; import { logger } from "../observability/logger.js"; /** * msg 服务 Kafka 客户端。 * * 仲裁依据: * - producer 配置 idempotent=true + transactionalId(at-least-once + 有序) * - consumer 使用 KAFKA_CONSUMER_GROUP_ID 隔离消费组 * - Kafka 不可用时服务仍启动(降级模式,outbox 消息暂存待发) * * 使用 lazy initialization:连接在首次 connect 时建立, * 避免模块导入副作用导致测试环境难以 mock。 */ let kafkaInstance: Kafka | null = null; let producerInstance: Producer | null = null; let consumerInstance: Consumer | null = null; let kafkaHealthy = false; function getKafka(): Kafka { if (!kafkaInstance) { kafkaInstance = new Kafka({ brokers: env.KAFKA_BROKERS.split(","), clientId: env.KAFKA_CLIENT_ID, }); } return kafkaInstance; } export function getProducer(): Producer { if (!producerInstance) { producerInstance = getKafka().producer({ idempotent: true, transactionalId: "msg-service-tx", }); } return producerInstance; } export function getConsumer(): Consumer { if (!consumerInstance) { consumerInstance = getKafka().consumer({ groupId: env.KAFKA_CONSUMER_GROUP_ID, sessionTimeout: 30000, rebalanceTimeout: 60000, }); } return consumerInstance; } export async function connectKafka(): Promise { try { await getProducer().connect(); await getConsumer().connect(); kafkaHealthy = true; logger.info( { brokers: env.KAFKA_BROKERS, clientId: env.KAFKA_CLIENT_ID }, "Kafka connected", ); } catch (err) { kafkaHealthy = false; logger.warn( { err }, "Kafka connect failed, running without Kafka (outbox will buffer)", ); } } /** /readyz 健康检查:Kafka producer+consumer 是否已连接 */ export function isKafkaHealthy(): boolean { return kafkaHealthy; } export async function disconnectKafka(): Promise { try { if (consumerInstance) { await consumerInstance.disconnect(); } if (producerInstance) { await producerInstance.disconnect(); } kafkaHealthy = false; logger.info("Kafka disconnected"); } catch (err) { logger.warn({ err }, "Kafka disconnect error (ignored on shutdown)"); } }