2026-01-17 18:37:44 +08:00
|
|
|
class StatsCounters {
|
|
|
|
|
constructor() {
|
2026-03-09 15:49:12 +08:00
|
|
|
// 原有 4 槽 + 新增 7 槽 = 11 槽
|
|
|
|
|
// [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 * 9);
|
2026-01-17 18:37:44 +08:00
|
|
|
this._minute = new BigInt64Array(this._minuteBuf);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 15:49:12 +08:00
|
|
|
_inc(slot, n = 1) {
|
2026-01-17 18:37:44 +08:00
|
|
|
const v = BigInt(Math.max(0, Number(n) || 0));
|
|
|
|
|
if (v === 0n) return;
|
2026-03-09 15:49:12 +08:00
|
|
|
Atomics.add(this._minute, slot, v);
|
2026-01-17 18:37:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 15:49:12 +08:00
|
|
|
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); }
|
2026-01-20 08:22:55 +08:00
|
|
|
|
2026-01-17 18:37:44 +08:00
|
|
|
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);
|
2026-01-20 08:22:55 +08:00
|
|
|
const dbWriteFailed = Atomics.exchange(this._minute, 3, 0n);
|
2026-03-09 15:49:12 +08:00
|
|
|
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);
|
|
|
|
|
return { dbWritten, filtered, kafkaPulled, dbWriteFailed, g4HotWritten, g4HotWriteFailed, roomStatusWritten, roomStatusFailed, g4HotErrorTableInserted };
|
2026-01-17 18:37:44 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-01-20 08:22:55 +08:00
|
|
|
this._lastFlushMinute = null;
|
2026-01-17 18:37:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-01-20 08:22:55 +08:00
|
|
|
const now = Date.now();
|
|
|
|
|
const minuteKey = Math.floor(now / 60_000);
|
|
|
|
|
if (this._lastFlushMinute === minuteKey) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-09 15:49:12 +08:00
|
|
|
const { dbWritten, filtered, kafkaPulled, dbWriteFailed, g4HotWritten, g4HotWriteFailed, roomStatusWritten, roomStatusFailed, g4HotErrorTableInserted } = this.stats.snapshotAndResetMinute();
|
2026-01-20 08:22:55 +08:00
|
|
|
this._lastFlushMinute = minuteKey;
|
2026-01-17 18:37:44 +08:00
|
|
|
const ts = formatTimestamp(new Date());
|
2026-03-09 15:49:12 +08:00
|
|
|
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} 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' } });
|
2026-01-17 18:37:44 +08:00
|
|
|
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 };
|
|
|
|
|
|