Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS public.idx_matches_share_code;

ALTER TABLE public.matches
DROP COLUMN IF EXISTS share_code;
10 changes: 10 additions & 0 deletions hasura/migrations/default/1875000000000_matches_share_code/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Our one stable handle on a Valve match: demo URLs expire, share codes don't,
-- so this is what lets us ask the GC about a match again later. It otherwise
-- only lives on pending_match_imports, which ParseImportedDemo deletes on a
-- successful import.
ALTER TABLE public.matches
ADD COLUMN IF NOT EXISTS share_code text;

CREATE INDEX IF NOT EXISTS idx_matches_share_code
ON public.matches (share_code)
WHERE share_code IS NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE public.pending_match_imports
ADD COLUMN IF NOT EXISTS parties jsonb;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Valve does not expose queue parties after a match ends. Verified at the wire
-- level: the reservation returned by MatchListRequestFullGameInfo contains only
-- account_ids, and the CS2 demo carries no party data anywhere. Nothing ever
-- wrote a non-null value here.
ALTER TABLE public.pending_match_imports
DROP COLUMN IF EXISTS parties;
29 changes: 15 additions & 14 deletions src/steam-match-history/jobs/ParseImportedDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { PostgresService } from "../../postgres/postgres.service";
import { DemoParserService } from "../../demos/demo-parser.service";
import { SteamMatchHistoryQueues } from "../enums/SteamMatchHistoryQueues";
import { MatchImportService } from "../match-import.service";
import { MatchParty } from "../types/MatchParty";

export type ParseImportedDemoPayload = {
valve_match_id: string;
Expand All @@ -16,7 +15,6 @@ export type ParseImportedDemoPayload = {
share_code?: string;
demo_url?: string | null;
match_start_time?: string | null;
parties?: MatchParty[] | null;
};

@UseQueue("SteamMatchHistory", SteamMatchHistoryQueues.ParseImportedDemo, {
Expand Down Expand Up @@ -45,6 +43,18 @@ export class ParseImportedDemo extends WorkerHost {
this.logger.log(
`parse-imported-demo skip valve_match_id=${valve_match_id}: match already imported (${existing}); admin must delete the match to re-import`,
);
// The pending row is about to go and it holds the only copy of the share
// code, which is our one stable handle on this match. Harvest it first.
const pending = await this.postgres.query<Array<{ share_code: string }>>(
`SELECT share_code
FROM public.pending_match_imports
WHERE valve_match_id = $1::numeric`,
[valve_match_id],
);
await this.matchImport.persistShareCode(
existing,
pending.at(0)?.share_code ?? job.data.share_code,
);
await this.postgres.query(
`DELETE FROM public.pending_match_imports WHERE valve_match_id = $1::numeric`,
[valve_match_id],
Expand All @@ -57,10 +67,9 @@ export class ParseImportedDemo extends WorkerHost {
share_code: string;
demo_url: string | null;
match_start_time: string | null;
parties: MatchParty[] | null;
}>
>(
`SELECT share_code, demo_url, match_start_time, parties
`SELECT share_code, demo_url, match_start_time
FROM public.pending_match_imports
WHERE valve_match_id = $1::numeric`,
[valve_match_id],
Expand All @@ -76,7 +85,6 @@ export class ParseImportedDemo extends WorkerHost {
row?.match_start_time ?? job.data.match_start_time ?? null;

const demoUrl = row?.demo_url ?? job.data.demo_url ?? null;
const parties = row?.parties ?? job.data.parties ?? null;

if (!demoUrl) {
this.logger.warn(
Expand All @@ -91,13 +99,7 @@ export class ParseImportedDemo extends WorkerHost {
[valve_match_id],
);

await this.runImport(
valve_match_id,
shareCode,
demoUrl,
matchStartTime,
parties,
);
await this.runImport(valve_match_id, shareCode, demoUrl, matchStartTime);
} catch (err) {
const lastAttempt =
(job.attemptsMade ?? 0) >= (job.opts.attempts ?? 1) - 1;
Expand All @@ -116,7 +118,6 @@ export class ParseImportedDemo extends WorkerHost {
shareCode: string | null,
demoUrl: string,
matchStartTime: string | null,
parties: MatchParty[] | null,
): Promise<void> {
const parsed = await this.demoParser.parseFromUrl(demoUrl);
if (!parsed) {
Expand All @@ -131,7 +132,7 @@ export class ParseImportedDemo extends WorkerHost {
demoUrl,
matchStartTime,
externalId: valveMatchId,
parties,
shareCode,
},
);
if (!result.matchId) {
Expand Down
39 changes: 16 additions & 23 deletions src/steam-match-history/jobs/ResolveMatchMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { UseQueue } from "src/utilities/QueueProcessors";
import { PostgresService } from "../../postgres/postgres.service";
import { SteamMatchHistoryQueues } from "../enums/SteamMatchHistoryQueues";
import { SteamGcService, ResolvedMatch } from "../steam-gc.service";
import { MatchParty } from "../types/MatchParty";
import { MatchImportService } from "../match-import.service";
import { ParseImportedDemo } from "./ParseImportedDemo";

Expand Down Expand Up @@ -38,10 +37,9 @@ export class ResolveMatchMetadata extends WorkerHost {
share_code: string;
demo_url: string | null;
match_start_time: string | null;
parties: MatchParty[] | null;
}>
>(
`SELECT share_code, demo_url, match_start_time, parties
`SELECT share_code, demo_url, match_start_time
FROM public.pending_match_imports
WHERE valve_match_id = $1::numeric`,
[valve_match_id],
Expand All @@ -57,7 +55,6 @@ export class ResolveMatchMetadata extends WorkerHost {
share_code: row.share_code,
demo_url: row.demo_url,
match_start_time: row.match_start_time,
parties: row.parties,
});
return;
}
Expand All @@ -79,42 +76,39 @@ export class ResolveMatchMetadata extends WorkerHost {
throw error;
}
if (!resolved) {
await this.markFailed(valve_match_id, "gc returned no demo url");
await this.markFailed(valve_match_id, "gc returned no match");
return;
}

const demoUrl = resolved.demoUrl ?? row.demo_url;

const matchStartTime =
resolved.matchStartTime ??
(await this.matchImport.resolveDemoStartTime(resolved.demoUrl));
row.match_start_time ??
(demoUrl ? await this.matchImport.resolveDemoStartTime(demoUrl) : null);

const partyCount = new Set(
(resolved.parties ?? []).map((party) => party.party_key),
).size;
this.logger.log(
`resolved valve_match_id=${valve_match_id} map=${resolved.mapName ?? "<none>"} matchStartTime=${matchStartTime ?? "<none>"} [source=${resolved.matchStartTime ? "gc-matchtime" : matchStartTime ? "demo-cdn-last-modified" : "none"}] parties=${partyCount} demoUrl=${resolved.demoUrl}`,
`resolved valve_match_id=${valve_match_id} map=${resolved.mapName ?? "<none>"} matchStartTime=${matchStartTime ?? "<none>"} [source=${resolved.matchStartTime ? "gc-matchtime" : matchStartTime ? "demo-cdn-last-modified" : "none"}] demoUrl=${demoUrl ?? "<none>"}`,
);

await this.postgres.query(
`UPDATE public.pending_match_imports
SET map_name = $2,
SET map_name = coalesce($2, map_name),
match_start_time = $3,
demo_url = $4,
parties = $5::jsonb
demo_url = $4
WHERE valve_match_id = $1::numeric`,
[
valve_match_id,
resolved.mapName,
matchStartTime,
resolved.demoUrl,
resolved.parties ? JSON.stringify(resolved.parties) : null,
],
[valve_match_id, resolved.mapName, matchStartTime, demoUrl],
);

if (!demoUrl) {
await this.markFailed(valve_match_id, "gc returned no demo url");
return;
}

await this.enqueueParse(valve_match_id, {
share_code: row.share_code,
demo_url: resolved.demoUrl,
demo_url: demoUrl,
match_start_time: matchStartTime,
parties: resolved.parties,
});
}

Expand All @@ -124,7 +118,6 @@ export class ResolveMatchMetadata extends WorkerHost {
share_code: string;
demo_url: string | null;
match_start_time: string | null;
parties: MatchParty[] | null;
},
): Promise<void> {
const jobId = `parse-${valveMatchId}`;
Expand Down
46 changes: 0 additions & 46 deletions src/steam-match-history/match-import.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ const computeStartingSides = (
}
).computeStartingSides;

const mapPartyIds = (
MatchImportService as unknown as {
mapPartyIds: (
parties: Array<{ steam_id: string; party_key: string }> | null,
) => Map<string, string>;
}
).mapPartyIds;

describe("MatchImportService.detectMatchType", () => {
it("uses the majority rank_type, not the first player", () => {
const players = [
Expand Down Expand Up @@ -228,41 +220,3 @@ describe("MatchImportService.computeStartingSides", () => {
expect(sides.get("A")).toBe("T");
});
});

describe("MatchImportService.mapPartyIds", () => {
it("gives everyone in the same source party one shared uuid", () => {
const ids = mapPartyIds([
{ steam_id: "A", party_key: "9" },
{ steam_id: "B", party_key: "9" },
{ steam_id: "C", party_key: "4" },
{ steam_id: "D", party_key: "4" },
]);

expect(ids.get("A")).toBe(ids.get("B"));
expect(ids.get("C")).toBe(ids.get("D"));
expect(ids.get("A")).not.toBe(ids.get("C"));
expect(ids.get("A")).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
});

it("does not reuse a source party key across imports", () => {
// Valve party ids are only unique within their own match, so two
// different matches that both saw party "9" must not look like the
// same group.
const first = mapPartyIds([
{ steam_id: "A", party_key: "9" },
{ steam_id: "B", party_key: "9" },
]);
const second = mapPartyIds([
{ steam_id: "C", party_key: "9" },
{ steam_id: "D", party_key: "9" },
]);
expect(first.get("A")).not.toBe(second.get("C"));
});

it("leaves players out when there is no party data", () => {
expect(mapPartyIds(null).size).toBe(0);
expect(mapPartyIds([]).size).toBe(0);
});
});
Loading
Loading