Files
Web_BLS_Heartbeat_Server/src/stats/statsManager.js
XuJiacheng 2f8857f98e feat: 添加 G5 独立写入功能
- 新增 G5 数据库连接配置与可关闭的写入开关
- 在现有 legacy/G4 写入成功路径后,追加独立的 G5 写入流程
- G5 使用与 G4 相同的数据结构映射,但不写入 guid,由数据库自生成 int4 guid
- room_status 新增 G5 独立 upsert 写入路径,并保留旧表与 G5 表的独立开关
- 新增 G5 写入统计与启动摘要输出
- 更新 StatsCounters 和 StatsReporter 以支持 G5 统计
- 增加测试覆盖,确保 G5 写入逻辑与 room_status 的独立执行
- 新增 G5 相关数据库表结构 SQL 文件
2026-03-10 16:29:24 +08:00

133 lines
5.8 KiB
JavaScript

class StatsCounters {
constructor() {
// [0] dbWritten, [1] filtered, [2] kafkaPulled, [3] dbWriteFailed,
// [4] g4HotWritten, [5] g4HotWriteFailed, [6] roomStatusWritten,
// [7] roomStatusFailed, [8] g4HotErrorTableInserted,
// [9] g5Written, [10] g5WriteFailed
// [0] dbWritten, [1] filtered, [2] kafkaPulled, [3] dbWriteFailed,
// [4] g4HotWritten, [5] g4HotWriteFailed, [6] roomStatusWritten,
// [7] roomStatusFailed, [8] g4HotErrorTableInserted
this._minuteBuf = new SharedArrayBuffer(BigInt64Array.BYTES_PER_ELEMENT * 11);
this._minute = new BigInt64Array(this._minuteBuf);
}
_inc(slot, n = 1) {
const v = BigInt(Math.max(0, Number(n) || 0));
if (v === 0n) return;
Atomics.add(this._minute, slot, v);
}
incDbWritten(n = 1) { this._inc(0, n); }
incFiltered(n = 1) { this._inc(1, n); }
incKafkaPulled(n = 1) { this._inc(2, n); }
incDbWriteFailed(n = 1) { this._inc(3, n); }
incG4HotWritten(n = 1) { this._inc(4, n); }
incG4HotWriteFailed(n = 1) { this._inc(5, n); }
incRoomStatusWritten(n = 1) { this._inc(6, n); }
incRoomStatusFailed(n = 1) { this._inc(7, n); }
incG4HotErrorTableInserted(n = 1) { this._inc(8, n); }
incG5Written(n = 1) { this._inc(9, n); }
incG5WriteFailed(n = 1) { this._inc(10, n); }
snapshotAndResetMinute() {
const dbWritten = Atomics.exchange(this._minute, 0, 0n);
const filtered = Atomics.exchange(this._minute, 1, 0n);
const kafkaPulled = Atomics.exchange(this._minute, 2, 0n);
const dbWriteFailed = Atomics.exchange(this._minute, 3, 0n);
const g4HotWritten = Atomics.exchange(this._minute, 4, 0n);
const g4HotWriteFailed = Atomics.exchange(this._minute, 5, 0n);
const roomStatusWritten = Atomics.exchange(this._minute, 6, 0n);
const roomStatusFailed = Atomics.exchange(this._minute, 7, 0n);
const g4HotErrorTableInserted = Atomics.exchange(this._minute, 8, 0n);
const g5Written = Atomics.exchange(this._minute, 9, 0n);
const g5WriteFailed = Atomics.exchange(this._minute, 10, 0n);
return { dbWritten, filtered, kafkaPulled, dbWriteFailed, g4HotWritten, g4HotWriteFailed, roomStatusWritten, roomStatusFailed, g4HotErrorTableInserted, g5Written, g5WriteFailed };
}
}
const pad2 = (n) => String(n).padStart(2, '0');
const pad3 = (n) => String(n).padStart(3, '0');
const formatTimestamp = (d) => {
const year = d.getFullYear();
const month = pad2(d.getMonth() + 1);
const day = pad2(d.getDate());
const hour = pad2(d.getHours());
const minute = pad2(d.getMinutes());
const second = pad2(d.getSeconds());
const ms = pad3(d.getMilliseconds());
return `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms}`;
};
class StatsReporter {
constructor({ redis, stats }) {
this.redis = redis;
this.stats = stats;
this._timer = null;
this._running = false;
this._lastFlushMinute = null;
}
start() {
if (this._running) return;
this._running = true;
this._scheduleNext();
}
stop() {
this._running = false;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
}
flushOnce() {
if (!this.redis?.isEnabled?.()) return;
const now = Date.now();
const minuteKey = Math.floor(now / 60_000);
if (this._lastFlushMinute === minuteKey) {
return;
}
const { dbWritten, filtered, kafkaPulled, dbWriteFailed, g4HotWritten, g4HotWriteFailed, roomStatusWritten, roomStatusFailed, g4HotErrorTableInserted, g5Written, g5WriteFailed } = this.stats.snapshotAndResetMinute();
this._lastFlushMinute = minuteKey;
const ts = formatTimestamp(new Date());
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} Legacy写入量: ${dbWritten}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} Legacy写入失败量: ${dbWriteFailed}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} G4Hot写入量: ${g4HotWritten}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} G4Hot写入失败量: ${g4HotWriteFailed}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} G5写入量: ${g5Written}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} G5写入失败量: ${g5WriteFailed}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} RoomStatus写入量: ${roomStatusWritten}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} RoomStatus失败量: ${roomStatusFailed}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} G4Hot错误表插入量: ${g4HotErrorTableInserted}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} 数据过滤量: ${filtered}`, metadata: { module: 'stats' } });
this.redis.pushConsoleLog?.({ level: 'info', message: `[STATS] ${ts} Kafka拉取量: ${kafkaPulled}`, metadata: { module: 'stats' } });
}
_scheduleNext() {
if (!this._running) return;
if (this._timer) return;
const now = Date.now();
const delay = 60_000 - (now % 60_000);
this._timer = setTimeout(() => {
this._timer = null;
try {
this.flushOnce();
} catch (err) {
this.redis?.pushConsoleLog?.({
level: 'warn',
message: `[ERROR] ${formatTimestamp(new Date())} 统计任务异常: ${String(err?.message ?? err)}`,
metadata: { module: 'stats' },
});
} finally {
this._scheduleNext();
}
}, delay);
}
}
export { StatsCounters, StatsReporter, formatTimestamp };