감사 정책 head 유실을 복구하고 데몬 준비 상태로 배포를 검증한다
This commit is contained in:
@@ -51,7 +51,7 @@ import { ReadModelOutboxWorker } from './realtime/outboxWorker.js';
|
||||
import { DeferredGeneralAccessWorker } from './services/deferredGeneralAccess.js';
|
||||
import { WebPushOutboxWorker } from './services/webPushOutboxWorker.js';
|
||||
import { scopeHttpIdempotencyKey } from './requestId.js';
|
||||
import { loadClockReadiness } from './services/clockReadiness.js';
|
||||
import { loadProfileReadiness } from './services/clockReadiness.js';
|
||||
|
||||
const extractBearerToken = (value: string | string[] | undefined): string | null => {
|
||||
if (!value) {
|
||||
@@ -436,14 +436,13 @@ export const createGameApiServer = async () => {
|
||||
});
|
||||
|
||||
app.get('/healthz', async (_request, reply) => {
|
||||
const clock = await loadClockReadiness(postgres.prisma);
|
||||
if (!clock.reconciliationComplete) reply.code(503);
|
||||
const readiness = await loadProfileReadiness(postgres.prisma, config.profileName);
|
||||
if (!readiness.ok) reply.code(503);
|
||||
return {
|
||||
ok: clock.reconciliationComplete,
|
||||
...readiness,
|
||||
profile: config.profileName,
|
||||
postgresPool: postgres.getPoolStats(),
|
||||
accountIconReconciliation: accountIconResetReconciler.getHealth(),
|
||||
clock,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseGameClockPhase } from '@sammo-ts/common';
|
||||
import { GamePrisma } from '@sammo-ts/infra';
|
||||
|
||||
import type { DatabaseClient } from '../context.js';
|
||||
|
||||
@@ -90,3 +91,20 @@ export const loadClockAdminStatus = async (db: DatabaseClient) => {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** API 기동과 별개로 데몬의 초기 감사 저장 및 clock 복구가 끝나야 배포 준비 완료다. */
|
||||
export const loadProfileReadiness = async (db: DatabaseClient, profileName: string) => {
|
||||
const [clock, leases] = await Promise.all([
|
||||
loadClockReadiness(db),
|
||||
db.$queryRaw<Array<{ ready: boolean }>>(GamePrisma.sql`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM turn_daemon_lease
|
||||
WHERE profile = ${profileName}
|
||||
AND clock_ready = TRUE
|
||||
AND lease_until > (clock_timestamp() AT TIME ZONE 'UTC')
|
||||
) AS ready
|
||||
`),
|
||||
]);
|
||||
const daemonReady = leases[0]?.ready === true;
|
||||
return { ok: clock.reconciliationComplete && daemonReady, clock, daemonReady };
|
||||
};
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DatabaseClient } from '../src/context.js';
|
||||
import { loadClockAdminStatus, loadClockReadiness } from '../src/services/clockReadiness.js';
|
||||
import { loadClockAdminStatus, loadClockReadiness, loadProfileReadiness } from '../src/services/clockReadiness.js';
|
||||
|
||||
describe('clock reconciliation readiness', () => {
|
||||
it.each([false, true])('requires the profile daemon lease to be ready: %s', async (ready) => {
|
||||
const db = {
|
||||
worldState: {
|
||||
findFirst: vi.fn(async () => ({ clockPhase: 'PREOPEN', clockRevision: 1n, deadlineGeneration: 0n })),
|
||||
},
|
||||
clockProjectionOutbox: { count: vi.fn(async () => 0) },
|
||||
$queryRaw: vi.fn(async () => [{ ready }]),
|
||||
} as unknown as DatabaseClient;
|
||||
await expect(loadProfileReadiness(db, 'che:default')).resolves.toMatchObject({
|
||||
ok: ready,
|
||||
daemonReady: ready,
|
||||
clock: { reconciliationComplete: true, gameplayEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed when the reconciliation schema is not available', async () => {
|
||||
const db = {} as DatabaseClient;
|
||||
await expect(loadClockReadiness(db)).resolves.toEqual({
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
|
||||
import { loadProfileReadiness } from '../src/services/clockReadiness.js';
|
||||
|
||||
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
||||
const integration = describe.skipIf(!databaseUrl);
|
||||
integration('profile deployment readiness', () => {
|
||||
let db: GamePrismaClient;
|
||||
let close: () => Promise<void>;
|
||||
beforeAll(async () => {
|
||||
const connector = createGamePostgresConnector({ url: databaseUrl! });
|
||||
await connector.connect();
|
||||
db = connector.prisma;
|
||||
close = () => connector.disconnect();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await close?.();
|
||||
});
|
||||
it('rejects absent, initializing, expired and other-profile leases; accepts ready paused/preopen runtime', async () => {
|
||||
const rollback = new Error('readiness fixture rollback');
|
||||
await expect(
|
||||
db.$transaction(async (tx) => {
|
||||
// Shared integration fixtures may leave projection work; rollback restores it.
|
||||
await tx.clockProjectionOutbox.deleteMany();
|
||||
await tx.worldState.create({
|
||||
data: {
|
||||
id: -998901,
|
||||
scenarioCode: 'readiness-fixture',
|
||||
currentYear: 190,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 300,
|
||||
clockPhase: 'PREOPEN',
|
||||
config: {},
|
||||
meta: {},
|
||||
},
|
||||
});
|
||||
const profile = 'readiness:fixture';
|
||||
expect((await loadProfileReadiness(tx, profile)).ok).toBe(false);
|
||||
await tx.turnDaemonLease.create({
|
||||
data: {
|
||||
profile,
|
||||
ownerId: 'fixture',
|
||||
fencingEpoch: 1n,
|
||||
heartbeatAt: new Date(),
|
||||
leaseUntil: new Date(Date.now() + 60_000),
|
||||
clockReady: false,
|
||||
},
|
||||
});
|
||||
expect((await loadProfileReadiness(tx, profile)).ok).toBe(false);
|
||||
await tx.turnDaemonLease.update({ where: { profile }, data: { clockReady: true } });
|
||||
expect((await loadProfileReadiness(tx, profile)).ok).toBe(true);
|
||||
expect((await loadProfileReadiness(tx, 'readiness:other')).ok).toBe(false);
|
||||
await tx.turnDaemonLease.update({
|
||||
where: { profile },
|
||||
data: { leaseUntil: new Date(Date.now() - 60_000) },
|
||||
});
|
||||
expect((await loadProfileReadiness(tx, profile)).ok).toBe(false);
|
||||
throw rollback;
|
||||
})
|
||||
).rejects.toBe(rollback);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user