feat(msg): 完整实现 msg 消息服务
包含 channels/preferences/templates/grpc/kafka/outbox/push/redis 等完整实现
This commit is contained in:
88
services/msg/src/shared/kafka/kafka.client.ts
Normal file
88
services/msg/src/shared/kafka/kafka.client.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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<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)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user