93 lines
4.1 KiB
JavaScript
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;
|