fix: recover auction finalization across worker restarts

This commit is contained in:
2026-07-31 11:59:02 +00:00
parent 439e4272e0
commit 1345aa25a1
8 changed files with 1202 additions and 146 deletions
+61 -53
View File
@@ -10,6 +10,8 @@ import {
import { resolveGameApiConfigFromEnv } from '../config.js';
import { DatabaseTurnDaemonTransport } from '../daemon/databaseTransport.js';
import { createBestEffortResourceCloser } from '../services/bestEffortResourceCloser.js';
import { createPollingWorkerControl, waitForWorkerPoll } from '../services/pollingWorkerLifecycle.js';
import type { TurnDaemonTransport } from '../daemon/transport.js';
import { buildTournamentKeys } from './keys.js';
import { TournamentStore } from './store.js';
@@ -31,7 +33,6 @@ import {
resolveGroupPair,
resolveNextAt,
seedNpcBets,
sleepMs,
sortByRanking,
type TournamentMatchOutcome,
type TournamentPrismaClient,
@@ -590,7 +591,11 @@ export const processTournamentTick = async (options: {
return processedState;
};
export const runTournamentWorker = async (): Promise<void> => {
export interface TournamentWorkerOptions {
signal?: AbortSignal;
}
export const runTournamentWorker = async (options: TournamentWorkerOptions = {}): Promise<void> => {
const config = resolveGameApiConfigFromEnv();
const postgres = createGamePostgresConnector(resolvePostgresConfigFromEnv({ schema: config.profile }));
const redis = createRedisConnector(resolveRedisConfigFromEnv());
@@ -600,61 +605,64 @@ export const runTournamentWorker = async (): Promise<void> => {
const store = new TournamentStore(redis.client, buildTournamentKeys(config.profileName));
const daemonTransport = new DatabaseTurnDaemonTransport(postgres.prisma, config.daemonRequestTimeoutMs);
const control = createPollingWorkerControl(options.signal);
const closeResources = createBestEffortResourceCloser([
{ name: 'tournament-worker-redis', run: () => redis.disconnect() },
{ name: 'tournament-worker-postgres', run: () => postgres.disconnect() },
]);
const handleExit = async () => {
await redis.disconnect();
await postgres.disconnect();
};
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);
try {
while (!control.signal.aborted) {
const state = await store.getState();
if (!state || (!state.auto && !needsSettlement(state))) {
await waitForWorkerPoll(control.signal, config.tournamentPollMs);
continue;
}
while (true) {
const state = await store.getState();
if (!state || (!state.auto && !needsSettlement(state))) {
await sleepMs(config.tournamentPollMs);
continue;
}
const nextAt = new Date(state.nextAt).getTime();
const now = Date.now();
if (state.auto && Number.isFinite(nextAt) && nextAt > now) {
await waitForWorkerPoll(control.signal, Math.min(config.tournamentPollMs, nextAt - now));
continue;
}
const nextAt = new Date(state.nextAt).getTime();
const now = Date.now();
if (state.auto && Number.isFinite(nextAt) && nextAt > now) {
await sleepMs(Math.min(config.tournamentPollMs, nextAt - now));
continue;
}
try {
await processTournamentTick({
store,
prisma: postgres.prisma,
daemonTransport,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
const trace = error instanceof Error ? error.stack : undefined;
const now = new Date().toISOString();
await postgres.prisma.errorLog.create({
data: {
category: 'TOURNAMENT',
source: 'tournament-worker',
message,
trace,
context: {
stage: state.stage,
phase: state.phase,
bettingId: state.bettingId ?? null,
nextAt: state.nextAt,
try {
await processTournamentTick({
store,
prisma: postgres.prisma,
daemonTransport,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
const trace = error instanceof Error ? error.stack : undefined;
const failedAt = new Date().toISOString();
await postgres.prisma.errorLog.create({
data: {
category: 'TOURNAMENT',
source: 'tournament-worker',
message,
trace,
context: {
stage: state.stage,
phase: state.phase,
bettingId: state.bettingId ?? null,
nextAt: state.nextAt,
},
},
},
});
const currentState = (await store.getState()) ?? state;
const nextState: TournamentState = {
...currentState,
lastError: message,
lastErrorAt: now,
};
await store.setState(nextState);
}
});
const currentState = (await store.getState()) ?? state;
const nextState: TournamentState = {
...currentState,
lastError: message,
lastErrorAt: failedAt,
};
await store.setState(nextState);
}
await sleepMs(config.tournamentPollMs);
await waitForWorkerPoll(control.signal, config.tournamentPollMs);
}
} finally {
control.dispose();
await closeResources();
}
};