Files
Edu/services/msg/src/shared/kafka/kafka.client.ts
SpecialX 7b7abbb309 feat(msg): 完整实现 msg 消息服务
包含 channels/preferences/templates/grpc/kafka/outbox/push/redis 等完整实现
2026-07-10 19:09:52 +08:00

89 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 + transactionalIdat-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<void> {
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<void> {
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)");
}
}