fix reserved turn daemon api serialization
This commit is contained in:
@@ -229,16 +229,25 @@ const claimGeneralRevision = async (
|
||||
expectedRevision: number
|
||||
): Promise<number> => {
|
||||
const nextRevision = expectedRevision + 1;
|
||||
const claimed =
|
||||
expectedRevision === 0
|
||||
? await db.generalTurnRevision.createMany({
|
||||
data: [{ generalId, revision: nextRevision }],
|
||||
skipDuplicates: true,
|
||||
})
|
||||
: await db.generalTurnRevision.updateMany({
|
||||
where: { generalId, revision: expectedRevision },
|
||||
data: { revision: nextRevision },
|
||||
});
|
||||
const now = new Date();
|
||||
let claimed = await db.generalTurnRevision.updateMany({
|
||||
where: {
|
||||
generalId,
|
||||
revision: expectedRevision,
|
||||
OR: [{ leaseOwner: null }, { leaseExpiresAt: { lte: now } }],
|
||||
},
|
||||
data: {
|
||||
revision: nextRevision,
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
},
|
||||
});
|
||||
if (claimed.count === 0 && expectedRevision === 0) {
|
||||
claimed = await db.generalTurnRevision.createMany({
|
||||
data: [{ generalId, revision: nextRevision }],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
if (claimed.count === 1) {
|
||||
return nextRevision;
|
||||
}
|
||||
@@ -253,16 +262,26 @@ const claimNationRevision = async (
|
||||
expectedRevision: number
|
||||
): Promise<number> => {
|
||||
const nextRevision = expectedRevision + 1;
|
||||
const claimed =
|
||||
expectedRevision === 0
|
||||
? await db.nationTurnRevision.createMany({
|
||||
data: [{ nationId, officerLevel, revision: nextRevision }],
|
||||
skipDuplicates: true,
|
||||
})
|
||||
: await db.nationTurnRevision.updateMany({
|
||||
where: { nationId, officerLevel, revision: expectedRevision },
|
||||
data: { revision: nextRevision },
|
||||
});
|
||||
const now = new Date();
|
||||
let claimed = await db.nationTurnRevision.updateMany({
|
||||
where: {
|
||||
nationId,
|
||||
officerLevel,
|
||||
revision: expectedRevision,
|
||||
OR: [{ leaseOwner: null }, { leaseExpiresAt: { lte: now } }],
|
||||
},
|
||||
data: {
|
||||
revision: nextRevision,
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
},
|
||||
});
|
||||
if (claimed.count === 0 && expectedRevision === 0) {
|
||||
claimed = await db.nationTurnRevision.createMany({
|
||||
data: [{ nationId, officerLevel, revision: nextRevision }],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
if (claimed.count === 1) {
|
||||
return nextRevision;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
const databaseUrl = process.env.RESERVED_TURN_DATABASE_URL;
|
||||
const describeIntegration = databaseUrl ? describe : describe.skip;
|
||||
const GENERAL_ID = 2_147_400_001;
|
||||
const REVISION_ZERO_GENERAL_ID = 2_147_400_002;
|
||||
|
||||
describeIntegration('reserved turn queue revision integration', () => {
|
||||
const connector = databaseUrl ? createGamePostgresConnector({ url: databaseUrl }) : null;
|
||||
@@ -20,16 +21,18 @@ describeIntegration('reserved turn queue revision integration', () => {
|
||||
return;
|
||||
}
|
||||
await connector.connect();
|
||||
await connector.prisma.generalTurn.deleteMany({ where: { generalId: GENERAL_ID } });
|
||||
await connector.prisma.generalTurnRevision.deleteMany({ where: { generalId: GENERAL_ID } });
|
||||
const generalIds = [GENERAL_ID, REVISION_ZERO_GENERAL_ID];
|
||||
await connector.prisma.generalTurn.deleteMany({ where: { generalId: { in: generalIds } } });
|
||||
await connector.prisma.generalTurnRevision.deleteMany({ where: { generalId: { in: generalIds } } });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!connector) {
|
||||
return;
|
||||
}
|
||||
await connector.prisma.generalTurn.deleteMany({ where: { generalId: GENERAL_ID } });
|
||||
await connector.prisma.generalTurnRevision.deleteMany({ where: { generalId: GENERAL_ID } });
|
||||
const generalIds = [GENERAL_ID, REVISION_ZERO_GENERAL_ID];
|
||||
await connector.prisma.generalTurn.deleteMany({ where: { generalId: { in: generalIds } } });
|
||||
await connector.prisma.generalTurnRevision.deleteMany({ where: { generalId: { in: generalIds } } });
|
||||
await connector.disconnect();
|
||||
});
|
||||
|
||||
@@ -60,4 +63,46 @@ describeIntegration('reserved turn queue revision integration', () => {
|
||||
expect(['che_훈련', 'che_사기진작']).toContain(snapshot.turns[0]?.action);
|
||||
expect(await connector.prisma.generalTurn.count({ where: { generalId: GENERAL_ID } })).toBe(30);
|
||||
});
|
||||
|
||||
it('uses an unlocked revision-zero lease row as the first API revision', async () => {
|
||||
if (!connector) {
|
||||
throw new Error('integration connector is unavailable');
|
||||
}
|
||||
|
||||
await connector.prisma.generalTurnRevision.create({
|
||||
data: {
|
||||
generalId: REVISION_ZERO_GENERAL_ID,
|
||||
revision: 0,
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await connector.prisma.$transaction((transaction) =>
|
||||
setGeneralTurn(
|
||||
transaction as unknown as DatabaseClient,
|
||||
REVISION_ZERO_GENERAL_ID,
|
||||
0,
|
||||
'che_훈련',
|
||||
{},
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.revision).toBe(1);
|
||||
expect(
|
||||
await connector.prisma.generalTurn.count({
|
||||
where: { generalId: REVISION_ZERO_GENERAL_ID },
|
||||
})
|
||||
).toBe(30);
|
||||
expect(
|
||||
await connector.prisma.generalTurnRevision.findUnique({
|
||||
where: { generalId: REVISION_ZERO_GENERAL_ID },
|
||||
})
|
||||
).toMatchObject({
|
||||
revision: 1,
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DatabaseClient, GeneralTurnRow, NationTurnRow } from '../src/context.js';
|
||||
import {
|
||||
@@ -338,4 +338,33 @@ describe('reservedTurns', () => {
|
||||
const noOpPush = await shiftNationTurns(db, 5, 12, 12, repeated.revision);
|
||||
expect(noOpPush).toEqual(repeated);
|
||||
});
|
||||
|
||||
it('rejects an API writer while the daemon holds the queue lease without touching turns', async () => {
|
||||
const deleteMany = vi.fn(async () => ({}));
|
||||
const createMany = vi.fn(async () => ({}));
|
||||
const db = {
|
||||
generalTurnRevision: {
|
||||
updateMany: vi.fn(async () => ({ count: 0 })),
|
||||
createMany: vi.fn(async () => ({ count: 0 })),
|
||||
findUnique: vi.fn(async () => ({
|
||||
generalId: 9,
|
||||
revision: 0,
|
||||
leaseOwner: 'daemon-1',
|
||||
leaseExpiresAt: new Date(Date.now() + 60_000),
|
||||
updatedAt: new Date(),
|
||||
})),
|
||||
},
|
||||
generalTurn: {
|
||||
findMany: vi.fn(async () => []),
|
||||
deleteMany,
|
||||
createMany,
|
||||
},
|
||||
} as unknown as DatabaseClient;
|
||||
|
||||
await expect(setGeneralTurn(db, 9, 0, 'che_훈련', {}, 0)).rejects.toBeInstanceOf(
|
||||
ReservedTurnRevisionConflictError
|
||||
);
|
||||
expect(deleteMany).not.toHaveBeenCalled();
|
||||
expect(createMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user