VM 복귀 후 예약턴 잠금 복구와 Windows 부팅 식별자 확인을 추가한다
This commit is contained in:
1 parent
fd8145d6eb
commit
f36bc2c8d7
24 files changed
+1048
-28
No files matched your search
@@ -4,7 +4,9 @@ import { createGamePostgresConnector } from '@sammo-ts/infra';
|
||||
import {
|
||||
InMemoryReservedTurnStore,
|
||||
ReservedTurnLeaseConflictError,
|
||||
createReservedTurnStore,
|
||||
} from '../src/turn/reservedTurnStore.js';
|
||||
import { createReservedTurnLeaseRetry } from '../src/turn/reservedTurnLeaseRetry.js';
|
||||
|
||||
const databaseUrl = process.env.RESERVED_TURN_DATABASE_URL;
|
||||
const describeIntegration = databaseUrl ? describe : describe.skip;
|
||||
@@ -132,4 +134,98 @@ describeIntegration('reserved turn daemon/API lease integration', () => {
|
||||
});
|
||||
expect(after.map((turn) => turn.actionCode)).toEqual(before.map((turn) => turn.actionCode));
|
||||
});
|
||||
|
||||
it('cleans persisted ownership after rollback loses the journal, preserves other writers, and reloads once', async () => {
|
||||
if (!connector || !databaseUrl) throw new Error('integration connector is unavailable');
|
||||
await connector.prisma.generalTurnRevision.update({
|
||||
where: { generalId: GENERAL_ID },
|
||||
data: { leaseOwner: null, leaseExpiresAt: null },
|
||||
});
|
||||
const before = await connector.prisma.generalTurn.findMany({
|
||||
where: { generalId: GENERAL_ID },
|
||||
orderBy: { turnIdx: 'asc' },
|
||||
});
|
||||
const revision = await connector.prisma.generalTurnRevision.findUniqueOrThrow({
|
||||
where: { generalId: GENERAL_ID },
|
||||
});
|
||||
const old = await createReservedTurnStore({ databaseUrl, leaseOwner: 'interrupted-daemon' });
|
||||
const savepoint = old.store.captureTransactionState();
|
||||
await old.store.prepareTurnsForExecution(GENERAL_ID);
|
||||
old.store.shiftGeneralTurns(GENERAL_ID, -1);
|
||||
old.store.restoreState(savepoint);
|
||||
expect(old.store.getQueueCounts().leasedGeneralQueues).toBe(0);
|
||||
const next = await createReservedTurnStore({ databaseUrl, leaseOwner: 'new-daemon' });
|
||||
try {
|
||||
await expect(next.store.prepareTurnsForExecution(GENERAL_ID)).rejects.toBeInstanceOf(
|
||||
ReservedTurnLeaseConflictError
|
||||
);
|
||||
await old.close();
|
||||
const released = await connector.prisma.generalTurnRevision.findUniqueOrThrow({
|
||||
where: { generalId: GENERAL_ID },
|
||||
});
|
||||
expect(released).toMatchObject({ leaseOwner: null, leaseExpiresAt: null, revision: revision.revision });
|
||||
await next.store.prepareTurnsForExecution(GENERAL_ID);
|
||||
// A delayed close of an old owner cannot clear the new owner's lease.
|
||||
const delayedCleaner = new InMemoryReservedTurnStore(connector.prisma, {
|
||||
maxGeneralTurns: 2,
|
||||
maxNationTurns: 1,
|
||||
leaseOwner: 'interrupted-daemon',
|
||||
});
|
||||
await delayedCleaner.releaseOwnedLeases();
|
||||
expect(
|
||||
await connector.prisma.generalTurnRevision.findUniqueOrThrow({ where: { generalId: GENERAL_ID } })
|
||||
).toMatchObject({ leaseOwner: 'new-daemon', revision: revision.revision });
|
||||
await next.store.releaseOwnedLeases();
|
||||
const actual = await connector.prisma.generalTurn.findMany({
|
||||
where: { generalId: GENERAL_ID },
|
||||
orderBy: { turnIdx: 'asc' },
|
||||
});
|
||||
expect(actual.map((row) => row.actionCode)).toEqual(before.map((row) => row.actionCode));
|
||||
} finally {
|
||||
await next.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('waits out an orphaned lease without deleting it and reloads an intervening API edit', async () => {
|
||||
if (!connector) throw new Error('integration connector is unavailable');
|
||||
await connector.prisma.generalTurnRevision.update({
|
||||
where: { generalId: GENERAL_ID },
|
||||
data: { leaseOwner: 'killed-daemon', leaseExpiresAt: new Date(Date.now() + 300_000) },
|
||||
});
|
||||
const store = new InMemoryReservedTurnStore(connector.prisma, {
|
||||
maxGeneralTurns: 2,
|
||||
maxNationTurns: 1,
|
||||
leaseOwner: 'recovery-daemon',
|
||||
});
|
||||
await store.loadAll();
|
||||
const retry = createReservedTurnLeaseRetry({
|
||||
sleep: async () => {
|
||||
// Simulate the recorded expiry, then an authorized API edit before the next claim.
|
||||
expect(
|
||||
await connector.prisma.generalTurnRevision.findUniqueOrThrow({ where: { generalId: GENERAL_ID } })
|
||||
).toMatchObject({ leaseOwner: 'killed-daemon' });
|
||||
await connector.prisma.generalTurnRevision.update({
|
||||
where: { generalId: GENERAL_ID },
|
||||
data: { leaseExpiresAt: new Date(0) },
|
||||
});
|
||||
await connector.prisma.generalTurn.updateMany({
|
||||
where: { generalId: GENERAL_ID, turnIdx: 0 },
|
||||
data: { actionCode: 'che_수련' },
|
||||
});
|
||||
},
|
||||
});
|
||||
try {
|
||||
let failure: unknown;
|
||||
try {
|
||||
await store.prepareTurnsForExecution(GENERAL_ID);
|
||||
} catch (error) {
|
||||
failure = error;
|
||||
}
|
||||
expect(await retry.wait(failure)).toBe(true);
|
||||
await store.prepareTurnsForExecution(GENERAL_ID);
|
||||
expect(store.getGeneralTurns(GENERAL_ID)[0]?.action).toBe('che_수련');
|
||||
} finally {
|
||||
await store.releaseOwnedLeases();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user