From d37acb4525d86306ef3fc1ae36132150b8eb1bd8 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Sun, 26 Jul 2026 15:41:54 -0400 Subject: [PATCH] bug: fix seeding / perf of sql on tele --- src/telemetry/telemetry.service.ts | 58 +++++++++++++++++++++--------- test/telemetry-perf.spec.ts | 18 +++++----- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/telemetry/telemetry.service.ts b/src/telemetry/telemetry.service.ts index 747398d0..6a4ada0d 100644 --- a/src/telemetry/telemetry.service.ts +++ b/src/telemetry/telemetry.service.ts @@ -109,6 +109,15 @@ export class TelemetryService { return (name: string) => (alias ? `${alias}.${name}` : name); } + private static activeLineups(interval: string) { + const recent = `${TelemetryService.nativeMatch("m")} + AND m.effective_at >= now() - interval '${interval}'`; + + return `SELECT m.lineup_1_id FROM public.matches m WHERE ${recent} + UNION + SELECT m.lineup_2_id FROM public.matches m WHERE ${recent}`; + } + constructor( private readonly logger: Logger, private readonly redisManagerService: RedisManagerService, @@ -752,10 +761,28 @@ export class TelemetryService { return row?.value; } + // Telemetry is the least important thing a panel does, so it is never allowed + // to hold a connection open. SET LOCAL scopes the timeout to this transaction + // rather than leaking onto a pooled connection the rest of the app reuses. A + // timeout aborts the whole collection and send() skips the hour. + private static readonly CollectTimeoutMs = 15_000; + + private async collectQuery(sql: string): Promise { + return await this.postgres.transaction(async (client) => { + await client.query( + `SET LOCAL statement_timeout = ${TelemetryService.CollectTimeoutMs}`, + ); + + const result = await client.query(sql); + + return result.rows as T; + }); + } + private async getSettings(): Promise> { - const rows = await this.postgres.query< - Array<{ name: string; value: string }> - >(`SELECT name, value FROM public.settings`); + const rows = await this.collectQuery>( + `SELECT name, value FROM public.settings`, + ); return new Map(rows.map(({ name, value }) => [name, value])); } @@ -770,9 +797,7 @@ export class TelemetryService { } private async getMatchesByType(): Promise> { - const rows = await this.postgres.query< - Array<{ type: string; count: string }> - >( + const rows = await this.collectQuery>( `SELECT o.type, count(*) AS count FROM public.matches m JOIN public.match_options o ON o.id = m.match_options_id @@ -784,7 +809,7 @@ export class TelemetryService { } private async getMatchesBySource(): Promise> { - const rows = await this.postgres.query< + const rows = await this.collectQuery< Array<{ source: string; count: string }> >( `SELECT source, count(*) AS count @@ -814,7 +839,7 @@ export class TelemetryService { } private async getCounts(): Promise { - const [row] = await this.postgres.query>( + const [row] = await this.collectQuery>( `SELECT (SELECT min(created_at) FROM public.matches) AS installed_at, @@ -883,20 +908,21 @@ export class TelemetryService { (SELECT count(*) FROM public.players) AS players_registered, (SELECT count(*) FROM public.teams) AS teams_total, + -- An OR across both lineup columns cannot use an index and forces a + -- join over every lineup row. Feeding the two sides in separately lets + -- this ride the (match_lineup_id, steam_id) unique index instead. (SELECT count(DISTINCT p.steam_id) FROM public.match_lineup_players p - JOIN public.matches m - ON m.lineup_1_id = p.match_lineup_id OR m.lineup_2_id = p.match_lineup_id WHERE p.steam_id IS NOT NULL - AND ${TelemetryService.nativeMatch("m")} - AND m.effective_at >= now() - interval '7 days') AS players_active_7d, + AND p.match_lineup_id IN ( + ${TelemetryService.activeLineups("7 days")} + )) AS players_active_7d, (SELECT count(DISTINCT p.steam_id) FROM public.match_lineup_players p - JOIN public.matches m - ON m.lineup_1_id = p.match_lineup_id OR m.lineup_2_id = p.match_lineup_id WHERE p.steam_id IS NOT NULL - AND ${TelemetryService.nativeMatch("m")} - AND m.effective_at >= now() - interval '30 days') AS players_active_30d, + AND p.match_lineup_id IN ( + ${TelemetryService.activeLineups("30 days")} + )) AS players_active_30d, (SELECT count(*) FROM public.tournaments) AS tournaments, (SELECT count(*) FROM public.league_seasons) AS league_seasons, diff --git a/test/telemetry-perf.spec.ts b/test/telemetry-perf.spec.ts index f02b5710..f6cf86c0 100644 --- a/test/telemetry-perf.spec.ts +++ b/test/telemetry-perf.spec.ts @@ -1,4 +1,6 @@ import { Logger } from "@nestjs/common"; + +const report = (line: string) => process.stdout.write(`PERF ${line}\n`); import { PostgresService } from "./../src/postgres/postgres.service"; import { bootMigratedDb, SqlTestDb } from "./utils/sql-test-db"; import { TelemetryService } from "./../src/telemetry/telemetry.service"; @@ -44,7 +46,7 @@ benchmark("telemetry perf", () => { const MATCHES = 20000; - console.log(`seeding ${MATCHES} matches...`); + report(`seeding ${MATCHES} matches...`); const started = Date.now(); await postgres.query( @@ -102,7 +104,7 @@ benchmark("telemetry perf", () => { FROM matches m`, ); - console.log(`seeded in ${Date.now() - started}ms`); + report(`seeded in ${Date.now() - started}ms`); const counts = await postgres.query>>( `SELECT @@ -110,7 +112,7 @@ benchmark("telemetry perf", () => { (SELECT count(*) FROM match_lineup_players) AS lineup_players, (SELECT count(*) FROM match_maps) AS maps`, ); - console.log("row counts:", counts[0]); + report(`row counts: ${JSON.stringify(counts[0])}`); await postgres.query("ANALYZE"); }, 900_000); @@ -123,7 +125,7 @@ benchmark("telemetry perf", () => { for (let i = 0; i < 3; i++) { const started = Date.now(); await service.collect(); - console.log(`collect() run ${i + 1}: ${Date.now() - started}ms`); + report(`collect() run ${i + 1}: ${Date.now() - started}ms`); } }, 300_000); @@ -138,12 +140,12 @@ benchmark("telemetry perf", () => { const started = Date.now(); await postgres.query(sql); - console.log(`OR-join active players: ${Date.now() - started}ms`); + report(`OR-join active players: ${Date.now() - started}ms`); const plan = await postgres.query>>( `EXPLAIN (ANALYZE, BUFFERS) ${sql}`, ); - console.log(plan.map((r) => r["QUERY PLAN"]).join("\n")); + report(plan.map((r) => r["QUERY PLAN"]).join("\n")); }, 300_000); it("times an IN-rewrite of the same thing", async () => { @@ -160,11 +162,11 @@ benchmark("telemetry perf", () => { const started = Date.now(); await postgres.query(sql); - console.log(`IN-rewrite active players: ${Date.now() - started}ms`); + report(`IN-rewrite active players: ${Date.now() - started}ms`); const plan = await postgres.query>>( `EXPLAIN (ANALYZE, BUFFERS) ${sql}`, ); - console.log(plan.map((r) => r["QUERY PLAN"]).join("\n")); + report(plan.map((r) => r["QUERY PLAN"]).join("\n")); }, 300_000); });