From 0299f15d1d33e2d76002115b2d412858fdf408bc Mon Sep 17 00:00:00 2001 From: zk Date: Mon, 18 May 2026 21:13:03 +0100 Subject: [PATCH 1/3] fix: lower log noise --- docker/myt-grpc-probe.sh | 19 ++ .../server/src/scripts/myt-grpc-probe.ts | 321 ++++++++++++++++++ .../server/src/server/router/ir/router.ts | 4 +- 3 files changed, 342 insertions(+), 2 deletions(-) create mode 100755 docker/myt-grpc-probe.sh create mode 100644 typescript/server/src/scripts/myt-grpc-probe.ts diff --git a/docker/myt-grpc-probe.sh b/docker/myt-grpc-probe.sh new file mode 100755 index 000000000..5d704e73b --- /dev/null +++ b/docker/myt-grpc-probe.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# MYT gRPC probe — run inside the tachi-server container. +# +# kubectl exec -it deploy/tachi-server -- sh /app/docker/myt-grpc-probe.sh \ +# --access-code "YOUR_ACCESS_CODE" --game chunithm +# +# Or paste the block from the bottom of this file if the script is not on the image yet. + +set -euo pipefail + +TACHI_ROOT="${TACHI_ROOT:-/app}" +cd "${TACHI_ROOT}/typescript/server" +exec bun run src/scripts/myt-grpc-probe.ts -- "$@" + +# --- paste into `kubectl exec ... -- sh` if this file is not on the image yet --- +# set -euo pipefail +# cd /app/typescript/server +# bun run src/scripts/myt-grpc-probe.ts -- --access-code "YOUR_ACCESS_CODE" --game chunithm +# bun run src/scripts/myt-grpc-probe.ts -- --profile-api-id "TITLE_API_ID" --game chunithm --max-items 1 diff --git a/typescript/server/src/scripts/myt-grpc-probe.ts b/typescript/server/src/scripts/myt-grpc-probe.ts new file mode 100644 index 000000000..340087ed6 --- /dev/null +++ b/typescript/server/src/scripts/myt-grpc-probe.ts @@ -0,0 +1,321 @@ +/** + * MYT gRPC probe: unary Cards.Lookup vs server-streaming GetPlaylog. + * + * Run inside the server container (same env as prod): + * + * cd /app/typescript/server + * bun run src/scripts/myt-grpc-probe.ts -- --profile-api-id "" --game chunithm + * bun run src/scripts/myt-grpc-probe.ts -- --access-code "..." --game chunithm + * + * Env (required): TACHI_MYT_API_HOST, TACHI_MYT_AUTH_TOKEN + * + * Exit 0 only if every requested phase succeeds. + */ +import type { Transport } from "@connectrpc/connect"; + +import { Cards, LookupRequestSchema } from "#proto/generated/cards/cards_pb"; +import { ChunithmUser, GetPlaylogRequestSchema } from "#proto/generated/chunithm/user_pb"; +import { + GetPlaylogRequestSchema as MaimaiGetPlaylogRequestSchema, + MaimaiUser, +} from "#proto/generated/maimai/user_pb"; +import { + GetPlaylogRequestSchema as OngekiGetPlaylogRequestSchema, + OngekiUser, +} from "#proto/generated/ongeki/user_pb"; +import { PlaylogRequestSchema, WaccaUser } from "#proto/generated/wacca/user_pb"; +import { create } from "@bufbuild/protobuf"; +import { Code, ConnectError, createClient } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { spawnSync } from "node:child_process"; +import { parseArgs } from "node:util"; + +type ProbeGame = "chunithm" | "maimaidx" | "ongeki" | "wacca"; + +const MYT_TITLE: Record = { + chunithm: "chunithm", + maimaidx: "maimai", + ongeki: "ongeki", + wacca: "wacca", +}; + +function log(section: string, message: string, extra?: Record): void { + const prefix = `[myt-probe:${section}]`; + if (extra === undefined) { + console.log(`${prefix} ${message}`); + } else { + console.log(`${prefix} ${message}`, JSON.stringify(extra, null, 2)); + } +} + +function fail(message: string): never { + console.error(`[myt-probe] ERROR: ${message}`); + process.exit(1); +} + +function requireEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) { + fail(`Missing env ${name}`); + } + return value; +} + +function createTransport(): Transport { + const host = requireEnv("TACHI_MYT_API_HOST"); + const token = requireEnv("TACHI_MYT_AUTH_TOKEN"); + + return createGrpcTransport({ + baseUrl: `https://${host}`, + defaultTimeoutMs: 10 * 60 * 1000, + interceptors: [ + (next) => async (req) => { + req.header.set("Authorization", `Bearer ${token}`); + return next(req); + }, + ], + }); +} + +function connectCodeName(code: Code): string { + return Code[code] ?? String(code); +} + +function formatConnectError(err: ConnectError): Record { + return { + message: err.message, + rawMessage: err.rawMessage, + code: err.code, + codeName: connectCodeName(err.code), + metadata: Object.fromEntries(err.metadata.entries()), + cause: + err.cause instanceof Error + ? { name: err.cause.name, message: err.cause.message, stack: err.cause.stack } + : err.cause, + }; +} + +async function probeLookup( + transport: Transport, + accessCode: string, + game: ProbeGame, +): Promise { + const client = createClient(Cards, transport); + const title = MYT_TITLE[game]; + const started = performance.now(); + + log("lookup", `Cards.Lookup titles=[${title}] ...`); + + try { + const response = await client.lookup( + create(LookupRequestSchema, { accessCode, titles: [title] }), + ); + + for (const entry of response.titles) { + if (entry.titleKind === title) { + const ms = Math.round(performance.now() - started); + log("lookup", `OK in ${ms}ms`, { + playerApiId: response.playerApiId, + titleApiId: entry.titleApiId, + titleKind: entry.titleKind, + }); + return entry.titleApiId; + } + } + + fail(`Lookup succeeded but no title "${title}" in response`); + } catch (err) { + const ms = Math.round(performance.now() - started); + if (err instanceof ConnectError) { + log("lookup", `FAILED after ${ms}ms`, formatConnectError(err)); + } else { + log("lookup", `FAILED after ${ms}ms`, { err: String(err) }); + } + throw err; + } +} + +async function probeGetPlaylog( + transport: Transport, + profileApiId: string, + game: ProbeGame, + maxItems: number, +): Promise { + const started = performance.now(); + let count = 0; + let firstItemMs: number | undefined; + + log("stream", `GetPlaylog (max ${maxItems} items) profileApiId=${profileApiId} ...`); + + const stream = createPlaylogStream(transport, profileApiId, game); + + try { + for await (const item of stream) { + count++; + if (firstItemMs === undefined) { + firstItemMs = Math.round(performance.now() - started); + log("stream", `First item after ${firstItemMs}ms`, { sample: summarizeItem(item) }); + } + + if (count >= maxItems) { + log("stream", `Stopping after ${count} item(s) (--max-items)`); + break; + } + } + + const ms = Math.round(performance.now() - started); + log("stream", `OK — received ${count} item(s) in ${ms}ms`, { + firstItemMs: firstItemMs ?? null, + }); + return count; + } catch (err) { + const ms = Math.round(performance.now() - started); + if (err instanceof ConnectError) { + log("stream", `FAILED after ${ms}ms (${count} item(s) before error)`, formatConnectError(err)); + } else { + log("stream", `FAILED after ${ms}ms (${count} item(s) before error)`, { err: String(err) }); + } + throw err; + } +} + +function summarizeItem(item: unknown): Record { + if (item === null || typeof item !== "object") { + return { type: typeof item }; + } + + const record = item as Record; + const summary: Record = {}; + if ("playlogApiId" in record) { + summary.playlogApiId = record.playlogApiId; + } + if ("info" in record && record.info !== undefined) { + summary.hasInfo = true; + } + if ("judge" in record && record.judge !== undefined) { + summary.hasJudge = true; + } + return summary; +} + +function createPlaylogStream( + transport: Transport, + profileApiId: string, + game: ProbeGame, +): AsyncIterable { + switch (game) { + case "chunithm": { + const client = createClient(ChunithmUser, transport); + return client.getPlaylog(create(GetPlaylogRequestSchema, { profileApiId })); + } + case "maimaidx": { + const client = createClient(MaimaiUser, transport); + return client.getPlaylog(create(MaimaiGetPlaylogRequestSchema, { profileApiId })); + } + case "ongeki": { + const client = createClient(OngekiUser, transport); + return client.getPlaylog(create(OngekiGetPlaylogRequestSchema, { profileApiId })); + } + case "wacca": { + const client = createClient(WaccaUser, transport); + return client.getPlaylog(create(PlaylogRequestSchema, { apiId: profileApiId })); + } + default: { + const _exhaustive: never = game; + return _exhaustive; + } + } +} + +function printRuntimeInfo(): void { + log("env", `Bun ${Bun.version}`); + log("env", `TACHI_MYT_API_HOST=${requireEnv("TACHI_MYT_API_HOST")}`); + + const node = spawnSync("node", ["--version"], { encoding: "utf8" }); + if (node.status === 0 && node.stdout.trim()) { + log("env", `Node ${node.stdout.trim()} (available for manual A/B vs bun)`); + } else { + log("env", "Node not on PATH — streaming A/B needs a Node binary in the image"); + } +} + +function printHelp(): void { + console.log(` +myt-grpc-probe — test MYT unary Lookup vs streaming GetPlaylog + + --profile-api-id Title API id (from Lookup or MYT admin) + --access-code Card access code (runs Lookup first) + --game chunithm | maimaidx | ongeki | wacca (default: chunithm) + --max-items Stop stream after N items (default: 5) + --lookup-only Skip GetPlaylog + --stream-only Skip Lookup (requires --profile-api-id) + -h, --help + +Examples (inside container): + + cd /app/typescript/server + bun run src/scripts/myt-grpc-probe.ts -- --access-code "YOUR_CODE" --game chunithm + bun run src/scripts/myt-grpc-probe.ts -- --profile-api-id "abc123" --max-items 1 +`); +} + +async function main(): Promise { + const { values } = parseArgs({ + options: { + "profile-api-id": { type: "string" }, + "access-code": { type: "string" }, + game: { type: "string", default: "chunithm" }, + "max-items": { type: "string", default: "5" }, + "lookup-only": { type: "boolean", default: false }, + "stream-only": { type: "boolean", default: false }, + help: { type: "boolean", short: "h", default: false }, + }, + allowPositionals: false, + }); + + if (values.help) { + printHelp(); + return; + } + + const game = values.game as ProbeGame; + if (!(game in MYT_TITLE)) { + fail(`Unknown --game ${values.game}; use chunithm, maimaidx, ongeki, or wacca`); + } + + const maxItems = Number.parseInt(values["max-items"] ?? "5", 10); + if (!Number.isFinite(maxItems) || maxItems < 1) { + fail("--max-items must be a positive integer"); + } + + printRuntimeInfo(); + + const transport = createTransport(); + let profileApiId = values["profile-api-id"]?.trim(); + + if (!values["stream-only"] && values["access-code"]) { + profileApiId = await probeLookup(transport, values["access-code"].trim(), game); + if (values["lookup-only"]) { + log("done", "Lookup-only mode — exit 0"); + return; + } + } + + if (values["lookup-only"]) { + if (!values["access-code"]) { + fail("--lookup-only requires --access-code"); + } + return; + } + + if (!profileApiId) { + fail("Need --profile-api-id or --access-code for GetPlaylog"); + } + + await probeGetPlaylog(transport, profileApiId, game, maxItems); + log("done", "All phases OK — exit 0"); +} + +main().catch(() => { + process.exit(1); +}); diff --git a/typescript/server/src/server/router/ir/router.ts b/typescript/server/src/server/router/ir/router.ts index 90ae792b6..3a3220c8e 100644 --- a/typescript/server/src/server/router/ir/router.ts +++ b/typescript/server/src/server/router/ir/router.ts @@ -22,7 +22,7 @@ const router: Router = Router({ mergeParams: true }); router.use(async (req, res, next) => { if (!req[SYMBOL_TACHI_API_AUTH]) { - log.info( + log.debug( { body: req.body, query: req.query, @@ -43,7 +43,7 @@ router.use(async (req, res, next) => { user = null; } - log.info( + log.debug( { user, body: req.body, From b74a87d3da480d28b91891d2e11e77569e9dada0 Mon Sep 17 00:00:00 2001 From: zk Date: Mon, 18 May 2026 20:16:20 +0000 Subject: [PATCH 2/3] fix: add ddr to env file --- typescript/server/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/server/.env b/typescript/server/.env index 2bb87dee0..70a2c92cc 100644 --- a/typescript/server/.env +++ b/typescript/server/.env @@ -48,7 +48,7 @@ TACHI_CDN_SAVE_LOCATION_PRIVATE_BUCKET=tachi-private TACHI_CDN_SAVE_LOCATION_PRIVATE_REGION=us-east-1 TACHI_NAME=Tachi Local Dev TACHI_TYPE=omni -TACHI_GAME_GROUPS=iidx,museca,sdvx,bms,chunithm,usc,wacca,popn,jubeat,pms,maimai,maimaidx,arcaea,ongeki +TACHI_GAME_GROUPS=iidx,museca,sdvx,bms,chunithm,usc,wacca,popn,jubeat,pms,maimai,maimaidx,arcaea,ongeki,ddr TACHI_IMPORT_TYPES=file/eamusement-iidx-csv,file/batch-manual,file/solid-state-squad,file/pli-iidx-csv,ir/direct-manual,ir/barbatos,ir/fervidex,ir/fervidex-static,ir/beatoraja,ir/usc,ir/kshook-sv6c,api/eag-iidx,api/eag-sdvx,api/flo-iidx,api/flo-sdvx,api/min-sdvx # TACHI_GITHUB_APP_ID= From 01be15ac6dbb5d633bf2e3ae9044eb52013baac4 Mon Sep 17 00:00:00 2001 From: zk Date: Mon, 18 May 2026 20:25:52 +0000 Subject: [PATCH 3/3] fix: misc linting errors --- .../src/lib/orphan-queue/deorphan-bms-pg.ts | 4 +--- typescript/server/src/scripts/myt-grpc-probe.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/typescript/server/src/lib/orphan-queue/deorphan-bms-pg.ts b/typescript/server/src/lib/orphan-queue/deorphan-bms-pg.ts index 631c9db5a..9633edc04 100644 --- a/typescript/server/src/lib/orphan-queue/deorphan-bms-pg.ts +++ b/typescript/server/src/lib/orphan-queue/deorphan-bms-pg.ts @@ -1,8 +1,6 @@ import { GetChartByIdForGame } from "#lib/db-formats/chart"; import { log } from "#lib/log/log"; -import { - LEGACY_CHART_ID_LENGTH, -} from "#lib/score-import/framework/score-importing/score-id"; +import { LEGACY_CHART_ID_LENGTH } from "#lib/score-import/framework/score-importing/score-id"; import DB from "#services/pg/db"; import { GetNextBmsPmsSongLegacyId } from "#utils/db"; import { Random20Hex } from "#utils/misc"; diff --git a/typescript/server/src/scripts/myt-grpc-probe.ts b/typescript/server/src/scripts/myt-grpc-probe.ts index 340087ed6..1093c8aae 100644 --- a/typescript/server/src/scripts/myt-grpc-probe.ts +++ b/typescript/server/src/scripts/myt-grpc-probe.ts @@ -11,8 +11,6 @@ * * Exit 0 only if every requested phase succeeds. */ -import type { Transport } from "@connectrpc/connect"; - import { Cards, LookupRequestSchema } from "#proto/generated/cards/cards_pb"; import { ChunithmUser, GetPlaylogRequestSchema } from "#proto/generated/chunithm/user_pb"; import { @@ -25,7 +23,7 @@ import { } from "#proto/generated/ongeki/user_pb"; import { PlaylogRequestSchema, WaccaUser } from "#proto/generated/wacca/user_pb"; import { create } from "@bufbuild/protobuf"; -import { Code, ConnectError, createClient } from "@connectrpc/connect"; +import { Code, ConnectError, createClient, type Transport } from "@connectrpc/connect"; import { createGrpcTransport } from "@connectrpc/connect-node"; import { spawnSync } from "node:child_process"; import { parseArgs } from "node:util"; @@ -171,9 +169,15 @@ async function probeGetPlaylog( } catch (err) { const ms = Math.round(performance.now() - started); if (err instanceof ConnectError) { - log("stream", `FAILED after ${ms}ms (${count} item(s) before error)`, formatConnectError(err)); + log( + "stream", + `FAILED after ${ms}ms (${count} item(s) before error)`, + formatConnectError(err), + ); } else { - log("stream", `FAILED after ${ms}ms (${count} item(s) before error)`, { err: String(err) }); + log("stream", `FAILED after ${ms}ms (${count} item(s) before error)`, { + err: String(err), + }); } throw err; } @@ -228,6 +232,8 @@ function createPlaylogStream( } function printRuntimeInfo(): void { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore log("env", `Bun ${Bun.version}`); log("env", `TACHI_MYT_API_HOST=${requireEnv("TACHI_MYT_API_HOST")}`);