Fix blank TPS: read tick timing from vanilla 'tick query' instead of spark

spark sends command output asynchronously, so its RCON responses are
empty and the sampler recorded null TPS/MSPT. Use the synchronous
vanilla 'tick query' as the primary source (treating paused/frozen idle
servers as healthy), keep spark as fallback, and route spark ping /
healthreport through the server console (mc-send-to-console) with the
log watcher capturing ping lines. Lag endpoint now falls back to the
freshest recorded ping sample.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Fable 5 committed 2026-07-10 12:39:46 -05:00
1 parent 85414391b0
commit 6b7839360d
4 files changed
+77 -9

No files matched your search

+14 -7
View File
@@ -1,7 +1,7 @@
import fs from "node:fs";
import { execFile } from "node:child_process";
import { inspectContainer, containerStats } from "./docker";
import { listOnlinePlayers, sparkTps, sparkPings } from "./mc";
import { inspectContainer, containerStats, execInContainer } from "./docker";
import { listOnlinePlayers, queryTps, sparkPings } from "./mc";
import { db } from "./db";
import { bus } from "./bus";
import { notify } from "./notify";
@@ -72,9 +72,9 @@ async function sampleTick(state: SamplerState) {
players = (await listOnlinePlayers()).online;
} catch {}
// spark TPS every 3rd tick (30s)
// TPS every 3rd tick (30s) — vanilla `tick query`, spark as fallback
if (state.tick % 3 === 0) {
const t = await sparkTps();
const t = await queryTps();
state.cachedTps = t.tps;
state.cachedMspt = t.mspt;
if (t.tps !== null && t.tps < 15) {
@@ -89,10 +89,17 @@ async function sampleTick(state: SamplerState) {
}
// per-player pings every 6th tick (60s)
if (state.tick % 6 === 0) {
if (state.tick % 6 === 0 && (players ?? 0) > 0) {
const pings = await sparkPings();
const insert = db().prepare("INSERT INTO ping_samples(ts, player, ping) VALUES(?, ?, ?)");
for (const [player, ping] of Object.entries(pings)) insert.run(ts, player, ping);
if (Object.keys(pings).length > 0) {
const insert = db().prepare("INSERT INTO ping_samples(ts, player, ping) VALUES(?, ?, ?)");
for (const [player, ping] of Object.entries(pings)) insert.run(ts, player, ping);
} else {
// spark replies asynchronously, so RCON often gets an empty response.
// Fall back to sending the command to the server console; the output
// lands in the log and the log watcher records the pings from there.
void execInContainer(env.mcContainer, ["mc-send-to-console", "spark", "ping"], 15_000).catch(() => {});
}
}
} else {
state.cachedTps = null;