- 新增 G5 数据库连接配置与可关闭的写入开关 - 在现有 legacy/G4 写入成功路径后,追加独立的 G5 写入流程 - G5 使用与 G4 相同的数据结构映射,但不写入 guid,由数据库自生成 int4 guid - room_status 新增 G5 独立 upsert 写入路径,并保留旧表与 G5 表的独立开关 - 新增 G5 写入统计与启动摘要输出 - 更新 StatsCounters 和 StatsReporter 以支持 G5 统计 - 增加测试覆盖,确保 G5 写入逻辑与 room_status 的独立执行 - 新增 G5 相关数据库表结构 SQL 文件
170 lines
6.1 KiB
JavaScript
170 lines
6.1 KiB
JavaScript
// 项目入口文件
|
||
import config from './config/config.js';
|
||
import { KafkaConsumer } from './kafka/consumer.js';
|
||
import { HeartbeatProcessor } from './processor/heartbeatProcessor.js';
|
||
import { DatabaseManager } from './db/databaseManager.js';
|
||
import { RedisIntegration } from './redis/redisIntegration.js';
|
||
import { StatsCounters, StatsReporter } from './stats/statsManager.js';
|
||
|
||
class WebBLSHeartbeatServer {
|
||
constructor() {
|
||
this.config = config;
|
||
this.kafkaConsumer = null;
|
||
this.heartbeatProcessor = null;
|
||
this.databaseManager = null;
|
||
this.g5DatabaseManager = null;
|
||
this.redis = null;
|
||
this.consumers = null;
|
||
this.stats = new StatsCounters();
|
||
this.statsReporter = null;
|
||
}
|
||
|
||
async start() {
|
||
try {
|
||
// 初始化 Redis(按协议写入心跳与控制台日志)
|
||
this.redis = new RedisIntegration(this.config.redis);
|
||
await this.redis.connect();
|
||
this.redis.startHeartbeat();
|
||
this.statsReporter = new StatsReporter({ redis: this.redis, stats: this.stats });
|
||
this.statsReporter.start();
|
||
|
||
// 初始化数据库连接
|
||
this.databaseManager = new DatabaseManager({ ...this.config.db, maxConnections: 1 });
|
||
await this.databaseManager.connect();
|
||
console.log('数据库连接成功');
|
||
await this.redis?.info('数据库连接成功', { module: 'db' });
|
||
|
||
if (this.config.g5db?.enabled) {
|
||
try {
|
||
this.g5DatabaseManager = new DatabaseManager({ ...this.config.g5db, maxConnections: 1 });
|
||
await this.g5DatabaseManager.connect();
|
||
console.log('G5数据库连接成功');
|
||
await this.redis?.info('G5数据库连接成功', { module: 'db', table: this.config.g5db?.g5Table });
|
||
} catch (error) {
|
||
this.g5DatabaseManager = null;
|
||
console.warn('G5数据库连接失败,已跳过 G5 写入:', error);
|
||
await this.redis?.warn('G5数据库连接失败,已跳过 G5 写入', {
|
||
module: 'db',
|
||
table: this.config.g5db?.g5Table,
|
||
error: String(error?.message ?? error),
|
||
});
|
||
}
|
||
}
|
||
|
||
// 打印双写配置摘要
|
||
const dbCfg = this.config.db;
|
||
const dualWriteSummary = {
|
||
legacyHeartbeat: dbCfg.legacyHeartbeatEnabled ? `ON → ${dbCfg.legacyTable}` : 'OFF',
|
||
g4HotHeartbeat: dbCfg.g4HotHeartbeatEnabled ? `ON → ${dbCfg.g4HotTable}` : 'OFF',
|
||
g5Heartbeat: this.config.g5db?.enabled ? `ON → ${this.config.g5db?.g5Table}` : 'OFF',
|
||
roomStatus: dbCfg.roomStatusEnabled !== false ? `ON → ${dbCfg.roomStatusTable}` : 'OFF',
|
||
g5RoomStatus: this.config.g5db?.roomStatusEnabled ? `ON → ${this.config.g5db?.roomStatusTable}` : 'OFF',
|
||
};
|
||
console.log('双写配置摘要:', dualWriteSummary);
|
||
await this.redis?.info('双写配置摘要', { module: 'db', ...dualWriteSummary });
|
||
|
||
// 打印 Kafka 配置摘要,便于排查连接问题
|
||
console.log('正在初始化 Kafka 消费者...');
|
||
console.log('Kafka 配置:', {
|
||
brokers: this.config.kafka?.brokers,
|
||
topics: this.config.kafka?.topics,
|
||
groupId: this.config.kafka?.groupId,
|
||
fromOffset: this.config.kafka?.fromOffset ?? 'latest',
|
||
ssl: !!this.config.kafka?.sslEnabled,
|
||
sasl: this.config.kafka?.saslEnabled ? `enabled (mechanism: ${this.config.kafka?.saslMechanism})` : 'disabled'
|
||
});
|
||
|
||
// 初始化处理器(共享批处理队列)
|
||
this.heartbeatProcessor = new HeartbeatProcessor(this.config.processor, this.databaseManager, {
|
||
g5DatabaseManager: this.g5DatabaseManager,
|
||
redis: this.redis,
|
||
stats: this.stats,
|
||
onDbOffline: () => {
|
||
if (this.consumers) {
|
||
this.consumers.forEach(c => c.consumer.pause());
|
||
}
|
||
},
|
||
onDbOnline: () => {
|
||
if (this.consumers) {
|
||
this.consumers.forEach(c => c.consumer.resume());
|
||
}
|
||
}
|
||
});
|
||
|
||
// 在单进程内启动 N 个消费者实例(与分区数匹配)
|
||
const instances = Math.max(1, Number(this.config.kafka?.consumerInstances ?? 1));
|
||
this.consumers = [];
|
||
for (let i = 0; i < instances; i++) {
|
||
const consumer = new KafkaConsumer(
|
||
{ ...this.config.kafka, consumerInstanceIndex: i },
|
||
this.heartbeatProcessor.processMessage.bind(this.heartbeatProcessor)
|
||
);
|
||
await consumer.connect();
|
||
await consumer.subscribe();
|
||
await consumer.startConsuming();
|
||
this.consumers.push({ consumer });
|
||
}
|
||
console.log(`Kafka消费者启动成功,共 ${instances} 个实例`);
|
||
await this.redis?.info('Kafka消费者启动成功', { module: 'kafka', topic: this.config.kafka?.topic, instances });
|
||
|
||
console.log('BLS心跳接收端启动成功');
|
||
await this.redis?.info('BLS心跳接收端启动成功', { module: 'app' });
|
||
} catch (error) {
|
||
console.error('启动失败:', error);
|
||
await this.redis?.error('启动失败', { module: 'app', error: String(error?.message ?? error) });
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async stop() {
|
||
try {
|
||
if (this.statsReporter) {
|
||
this.statsReporter.stop();
|
||
this.statsReporter = null;
|
||
}
|
||
|
||
if (this.consumers && Array.isArray(this.consumers)) {
|
||
for (const { consumer } of this.consumers) {
|
||
await consumer.stopConsuming();
|
||
await consumer.disconnect();
|
||
}
|
||
this.consumers = null;
|
||
}
|
||
|
||
if (this.databaseManager) {
|
||
await this.databaseManager.disconnect();
|
||
}
|
||
|
||
if (this.g5DatabaseManager) {
|
||
await this.g5DatabaseManager.disconnect();
|
||
}
|
||
|
||
if (this.redis) {
|
||
await this.redis.info('BLS心跳接收端已停止', { module: 'app' });
|
||
await this.redis.disconnect();
|
||
}
|
||
|
||
console.log('BLS心跳接收端已停止');
|
||
} catch (error) {
|
||
console.error('停止失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 启动服务器
|
||
const server = new WebBLSHeartbeatServer();
|
||
server.start();
|
||
|
||
// 处理进程终止信号
|
||
process.on('SIGINT', () => {
|
||
server.stop();
|
||
process.exit(0);
|
||
});
|
||
|
||
process.on('SIGTERM', () => {
|
||
server.stop();
|
||
process.exit(0);
|
||
});
|
||
|
||
export { WebBLSHeartbeatServer };
|