Files
core2026/tools/play-audit-maintenance.mjs
T

93 lines
4.1 KiB
JavaScript

#!/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;