감사 상세를 무손실 압축하고 이전 기수 정리 처리량을 개선한다
This commit is contained in:
1 parent
da79013226
commit
078b9aa205
23 files changed
+504
-95
No files matched your search
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { asRecord, type TurnCheckpoint } from '@sammo-ts/common';
|
||||
import { createGamePostgresConnector } from '@sammo-ts/infra';
|
||||
import { createGamePostgresConnector, decodeAuditDecisionSteps } from '@sammo-ts/infra';
|
||||
import { createTurnDaemonRuntime, seedScenarioToDatabase } from '@sammo-ts/game-engine';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
@@ -39,7 +39,7 @@ const verify = async (progress: unknown[]) => {
|
||||
include: { chunks: { orderBy: { ordinal: 'asc' } } },
|
||||
});
|
||||
assert(row && row.chunks.length, `Missing actual NPC ${JSON.stringify(selection)} decision`);
|
||||
const steps = row.chunks.flatMap((chunk) => (Array.isArray(chunk.steps) ? chunk.steps : []));
|
||||
const steps = row.chunks.flatMap((chunk) => decodeAuditDecisionSteps(chunk.steps));
|
||||
assert.equal(steps.length, row.stepCount);
|
||||
assert.equal(asRecord(steps[0]).kind, 'DECISION_START');
|
||||
assert(steps.some((step) => asRecord(step).kind === 'DECISION_END'));
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
// stopped/reserved profile도 같은 fenced batch로 정리한다. 기본은 관찰만 하며 외부 scheduler가 실행한다.
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { createGamePostgresConnector, pruneDeliveredReadModelOutbox } from '../packages/infra/dist/index.js';
|
||||
import { prunePreviousAuditBatch } from '../app/game-engine/dist/index.js';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const apply = args.includes('--apply');
|
||||
const schemas =
|
||||
args
|
||||
.find((arg) => arg.startsWith('--schemas='))
|
||||
?.slice(10)
|
||||
.split(',') ?? [];
|
||||
const budgetMs = Number(args.find((arg) => arg.startsWith('--budget-ms='))?.slice(12) ?? 3000);
|
||||
if (
|
||||
!schemas.length ||
|
||||
schemas.some((schema) => !/^[a-z][a-z0-9_]{0,62}$/.test(schema)) ||
|
||||
!Number.isSafeInteger(budgetMs) ||
|
||||
budgetMs < 1 ||
|
||||
budgetMs > 60000 ||
|
||||
args.some((arg) => arg !== '--apply' && !arg.startsWith('--schemas=') && !arg.startsWith('--budget-ms='))
|
||||
) {
|
||||
throw new Error('Usage: --schemas=che,kwe,... [--budget-ms=3000] [--apply]');
|
||||
}
|
||||
let failed = false;
|
||||
for (const schema of schemas) {
|
||||
const url = process.env.DATABASE_URL ? new URL(process.env.DATABASE_URL) : new URL('postgresql://localhost');
|
||||
if (!process.env.DATABASE_URL) {
|
||||
url.hostname = process.env.POSTGRES_HOST ?? 'localhost';
|
||||
url.port = process.env.POSTGRES_PORT ?? '5432';
|
||||
url.username = process.env.POSTGRES_USER ?? 'sammo';
|
||||
url.password = process.env.POSTGRES_PASSWORD ?? '';
|
||||
url.pathname = `/${process.env.POSTGRES_DB ?? 'sammo'}`;
|
||||
}
|
||||
url.searchParams.set('schema', schema);
|
||||
const connector = createGamePostgresConnector({ url: url.href, maxConnections: 1 });
|
||||
try {
|
||||
await connector.connect();
|
||||
const db = connector.prisma;
|
||||
const world = await db.worldState.findFirst({ select: { meta: true } });
|
||||
const serverId =
|
||||
world?.meta && typeof world.meta === 'object' && !Array.isArray(world.meta) ? world.meta.serverId : null;
|
||||
if (typeof serverId !== 'string' || !serverId.trim()) {
|
||||
console.log(JSON.stringify({ schema, status: 'identityMissing', deleted: 0 }));
|
||||
continue;
|
||||
}
|
||||
if (!apply) {
|
||||
console.log(JSON.stringify({ schema, status: 'dryRun', apply: false }));
|
||||
continue;
|
||||
}
|
||||
const deadline = performance.now() + budgetMs;
|
||||
let auditDeleted = 0;
|
||||
let outboxDeleted = 0;
|
||||
let status = 'progress';
|
||||
let auditComplete = false;
|
||||
let outboxComplete = false;
|
||||
do {
|
||||
if (!auditComplete) {
|
||||
const result = await prunePreviousAuditBatch(db, serverId);
|
||||
auditDeleted += result.deleted;
|
||||
status = result.status;
|
||||
auditComplete = result.status === 'complete';
|
||||
if (result.status === 'identityChanged' || result.status === 'busy') break;
|
||||
}
|
||||
if (!outboxComplete) {
|
||||
outboxDeleted += await db.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SET LOCAL statement_timeout='2000ms'`;
|
||||
await tx.$executeRaw`SET LOCAL lock_timeout='250ms'`;
|
||||
const deleted = await pruneDeliveredReadModelOutbox(tx, {
|
||||
deliveredBefore: new Date(Date.now() - 86400000),
|
||||
limit: 500,
|
||||
});
|
||||
outboxComplete = deleted < 500;
|
||||
return deleted;
|
||||
});
|
||||
}
|
||||
if (auditComplete && outboxComplete) break;
|
||||
await setTimeout(25);
|
||||
} while (performance.now() < deadline);
|
||||
console.log(
|
||||
JSON.stringify({ schema, status, auditDeleted, outboxDeleted, complete: auditComplete && outboxComplete })
|
||||
);
|
||||
} catch {
|
||||
// 연결 URL과 원문 SQL을 출력하지 않고 scheduler가 감지할 수 있는 종료 코드를 남긴다.
|
||||
failed = true;
|
||||
console.error(JSON.stringify({ schema, status: 'failed' }));
|
||||
} finally {
|
||||
await connector.disconnect();
|
||||
}
|
||||
}
|
||||
if (failed) process.exitCode = 1;
|
||||
Reference in new issue
Block a user