test: 감사 저장 batch의 실제 SQL 실행 횟수를 검증한다
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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는 여전히 남는다.
|
||||
|
||||
|
||||
### 정책·외교 사건의 요청 처리 상태
|
||||
|
||||
Reference in New Issue
Block a user