test: 감사 저장 batch의 실제 SQL 실행 횟수를 검증한다

This commit is contained in:
2026-09-16 09:11:27 +00:00
parent ef1ba055b5
commit c9fe204c94
2 changed files with 35 additions and 5 deletions
@@ -1,7 +1,7 @@
import { writeFile } from 'node:fs/promises';
import { performance } from 'node:perf_hooks';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
import { createGamePostgresConnector, type GamePrisma, type GamePrismaClient } from '@sammo-ts/infra';
import { persistAuditDecisions } from '../src/playAudit/decisionPersistence.js';
import { buildAuditDecisionFixture } from './fixtures/playAuditDecision.js';
@@ -9,12 +9,26 @@ const databaseUrl = process.env.PLAY_AUDIT_COST_DATABASE_URL;
describe.skipIf(!databaseUrl)('decision storage cost probe', () => {
let db: GamePrismaClient;
let close: () => Promise<void>;
let recording = false;
let statements: Record<string, number> = {};
beforeAll(async () => {
if (new URL(databaseUrl!).searchParams.get('schema') !== 'play_audit_cost_decision_fixture')
throw new Error('Dedicated audit cost fixture required');
const connector = createGamePostgresConnector({ url: databaseUrl!, maxConnections: 1 });
const connector = createGamePostgresConnector({
url: databaseUrl!,
maxConnections: 1,
log: [{ emit: 'event', level: 'query' }],
});
await connector.connect();
db = connector.prisma;
// connector의 반환 타입은 log generic을 지운다. 위에서 활성화한 event만 구독한다.
const observedDb = db as GamePrismaClient<GamePrisma.PrismaClientOptions, 'query'>;
observedDb.$on('query', (event) => {
if (!recording) return;
// 원문 SQL/params는 보존하거나 출력하지 않고 명령 종류만 집계한다.
const command = event.query.trim().split(/\s+/, 1)[0]!.toUpperCase();
statements[command] = (statements[command] ?? 0) + 1;
});
close = () => connector.disconnect();
await db.playAuditDecisionChunk.deleteMany();
await db.playAuditDecision.deleteMany();
@@ -27,9 +41,19 @@ describe.skipIf(!databaseUrl)('decision storage cost probe', () => {
return { ...decision, tick: decision.tick + index };
});
const payloadBytes = Buffer.byteLength(JSON.stringify(decisions));
statements = {};
recording = true;
const start = performance.now();
await db.$transaction((tx) => persistAuditDecisions(tx, decisions), { timeout: 30_000 });
try {
await db.$transaction((tx) => persistAuditDecisions(tx, decisions), { timeout: 30_000 });
} finally {
recording = false;
}
const writeMs = performance.now() - start;
const writeStatements = { ...statements };
// 200결정: header1 + chunk3, 나머지1결정: header1 + chunk1. hash 조회는 batch당1.
expect(writeStatements.INSERT).toBe(6);
expect(writeStatements.SELECT).toBe(2);
expect(await db.playAuditDecision.count()).toBe(201);
expect(await db.playAuditDecisionChunk.count()).toBe(603);
const [storage] = await db.$queryRaw<
@@ -75,7 +99,7 @@ describe.skipIf(!databaseUrl)('decision storage cost probe', () => {
`;
const metrics = {
scope: 'synthetic 201 decisions, 302 steps each; isolated schema, warm local reads',
limitations: 'Not production p95; no gameplay baseline, SQL count, WAL or retained heap measurement. Repetitive synthetic steps compress well. Relation allocation can retain space from previous runs.',
limitations: 'Not production p95; no gameplay baseline, WAL or retained heap measurement. SQL counts cover only the persistence transaction. Repetitive synthetic steps compress well. Relation allocation can retain space from previous runs.',
decisions: 201,
steps: Number(storage!.steps),
chunks: 603,
@@ -89,6 +113,7 @@ describe.skipIf(!databaseUrl)('decision storage cost probe', () => {
totalBytes: Number(row.totalBytes),
})),
writeMs,
writeStatements,
samples: timings.length,
readP50Ms: timings[14],
readP95Ms: timings[28],
+6 -1
View File
@@ -29,7 +29,12 @@ p50/p95와 EXPLAIN ANALYZE BUFFERS를 `/tmp/play-audit-decision-cost.json`에
2026-09-16 격리 PG 결과는 JSON12,271,836B, header행101,304B/chunk행666,924B,
저장677.56ms, 목록51건 p50 0.76ms/p95 1.01ms였다. 월별 장수 index scan으로51행을
읽었다. 반복 RNG 합성 fixture는 압축률이 높으므로 평균 운영 trace 크기의 근거가 아니다.
SQL 왕복·WAL·retained heap, 실제 scenario 계측 전후와 전체 COST gate는 여전히 남는다.
추가 query event 계측에서는 동일201결정의 저장에 INSERT6회(헤더2/chunk4),
SELECT2회(hash 확인), COMMIT1회가 관측됐다. BEGIN 등 driver 내부 통신은 event에
나타나지 않으므로 네트워크 왕복 전체로 해석하지 않는다. test는 INSERT/SELECT 수를
assert하며 SQL/params 원문은 저장하지 않는다. 관측60,702단계마다 SQL을 쓰지 않고 batch당
저장·hash 조회를 확인한 범위다. WAL·retained heap, 실제 scenario 계측 전후와 전체
COST gate는 여전히 남는다.
### 정책·외교 사건의 요청 처리 상태