[match] add basic stat table for fun

This commit is contained in:
Devin Zuczek
2026-08-30 00:44:50 -04:00
parent a620cadde0
commit e07c52ced7
5 changed files with 97 additions and 4 deletions
+16
View File
@@ -0,0 +1,16 @@
-- Server statistics sampled over time — one row per sample, as the presence cron
-- writes them. Generated from packages/domain/src/stats-db.ts (STAT_SCHEMA_DDL) — keep
-- in sync.
--
-- `stat_type` names what was measured (currently just `online`: the number of live
-- `presence` rows, i.e. players online, taken right after the expired ones are swept).
-- `value` is the measurement and `datetime` is when it was taken, as an ISO-8601 UTC
-- string so it reads directly and sorts lexically.
CREATE TABLE IF NOT EXISTS stat (
stat_type TEXT NOT NULL,
value INTEGER NOT NULL,
datetime TEXT NOT NULL
);
-- For "the `online` series over a time range".
CREATE INDEX IF NOT EXISTS idx_stat_type_datetime ON stat (stat_type, datetime);
+7 -1
View File
@@ -6,6 +6,7 @@ import {
Accessibility,
areFriends,
canManageRoom,
countOnlinePlayers,
createRoomInstance,
createRoomInvite,
deleteEmptyRoomInstances,
@@ -35,6 +36,7 @@ import {
MessageType,
MOST_ACTIVE_CLUBHOUSE_LIMIT,
recordRoomVisit,
recordStat,
refreshInstanceFullness,
RoomInstanceType,
setPresence,
@@ -2903,10 +2905,14 @@ async function sweepExpiredPresence(env: Env): Promise<void> {
for (const instanceId of staleInstanceIds) {
await refreshInstanceFullness(env.DB, instanceId)
}
// Sample the player count into `stat` — taken after the purge, so it's the live
// rows and not the ones that just lapsed. One row per cron run: the `online` series.
const online = await countOnlinePlayers(env.DB)
await recordStat(env.DB, 'online', online)
// The tagged logger is request-scoped (its middleware never runs for a cron), so
// log plainly here — Workers observability picks it up either way.
console.log(
`presence sweep: removed ${removed} expired rows, deleted ${emptyInstanceIds.length} empty instances, refreshed ${staleInstanceIds.length} instances`
`presence sweep: removed ${removed} expired rows, deleted ${emptyInstanceIds.length} empty instances, refreshed ${staleInstanceIds.length} instances, ${online} online`
)
}
+28 -3
View File
@@ -20,6 +20,7 @@ import {
ROOM_SCHEMA_DDL,
seedRoomWithSubRooms,
setPresence,
STAT_SCHEMA_DDL,
SUBROOM_SCHEMA_DDL,
} from '@repo/domain'
@@ -124,6 +125,8 @@ beforeAll(async () => {
for (const stmt of PRESENCE_SCHEMA_DDL) await env.DB.prepare(stmt).run()
// Room invites (owned by this worker) — POST /invite mints a row per invite.
for (const stmt of ROOM_INVITE_SCHEMA_DDL) await env.DB.prepare(stmt).run()
// Stats (owned by this worker) — the presence cron samples the online count into it.
for (const stmt of STAT_SCHEMA_DDL) await env.DB.prepare(stmt).run()
// Accounts table (owned by the auth worker) — dorm creation reads the username
// to name the room. Seed the players the dorm tests authenticate as.
@@ -1909,6 +1912,30 @@ describe('auth-gated endpoints', () => {
expect((await getRoomInstance(env.DB, solo))?.isFull).toBe(false)
})
test('records an `online` stat sample of the live presence count on each run', async () => {
await env.DB.prepare('DELETE FROM stat').run()
const before = (await env.DB.prepare('SELECT COUNT(*) AS n FROM presence WHERE expires_at > ?1')
.bind(nowSeconds())
.first<{ n: number }>())!.n
const ctx = createExecutionContext()
await scheduled(createScheduledController(), env, ctx)
await waitOnExecutionContext(ctx)
const rows = (
await env.DB.prepare('SELECT stat_type, value, datetime FROM stat').all<{
stat_type: string
value: number
datetime: string
}>()
).results
expect(rows).toHaveLength(1)
expect(rows[0]!.stat_type).toBe('online')
expect(rows[0]!.value).toBe(before)
// Stamped with the current time, as ISO-8601.
expect(Math.abs(Date.parse(rows[0]!.datetime) - Date.now())).toBeLessThan(10_000)
})
// Age an instance past EMPTY_INSTANCE_GRACE_SECONDS by backdating its `createdAt`
// (the generated `created_at` column follows the blob), so the empty-instance sweep
// can be exercised without waiting out the grace window.
@@ -2444,9 +2471,7 @@ describe('auth-gated endpoints', () => {
// The one frame an invite from a client on `version` pushes, with the RoomInviteId
// the call answered — a v2 invite names it in its Data.
const inviteFrom = async (
version?: string
): Promise<{ frame: Sent; roomInviteId: number }> => {
const inviteFrom = async (version?: string): Promise<{ frame: Sent; roomInviteId: number }> => {
await hub().fetch('http://do/all', { method: 'DELETE' })
const res = await exports.default.fetch(`${ORIGIN}/invite`, {
method: 'POST',