토너먼트 참가 트랜잭션 교착을 해소한다
This commit is contained in:
@@ -103,6 +103,9 @@ const withTournamentBetClockMutation = async <T>(
|
||||
const tournamentBetCommandRequestId = (requestId: string | undefined, step: string): string | undefined =>
|
||||
requestId ? `${requestId}:tournamentBet:${step}` : undefined;
|
||||
|
||||
const tournamentJoinCommandRequestId = (requestId: string | undefined, step: string): string | undefined =>
|
||||
requestId ? `${requestId}:tournamentJoin:${step}` : undefined;
|
||||
|
||||
const zTournamentState = z.object({
|
||||
stage: z.number().int().min(0),
|
||||
phase: z.number().int().min(0),
|
||||
@@ -453,7 +456,9 @@ export const tournamentRouter = router({
|
||||
|
||||
return { state, totals, myTotals, totalAmount, myAmount };
|
||||
}),
|
||||
join: authedProcedure.mutation(async ({ ctx }) => {
|
||||
// 참가비는 ENGINE transaction이 차감한다. API input-event transaction으로
|
||||
// 감싸면 clock advisory lock을 쥔 채 child ENGINE event를 기다리게 된다.
|
||||
join: engineAuthedProcedure.mutation(async ({ ctx }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||
return withTournamentClockMutation(ctx, store, async () => {
|
||||
@@ -476,6 +481,7 @@ export const tournamentRouter = router({
|
||||
const develCost = resolveCurrentDevelCost(worldState);
|
||||
const feeResult = await ctx.turnDaemon.requestCommand({
|
||||
type: 'adjustGeneralResources',
|
||||
requestId: tournamentJoinCommandRequestId(ctx.requestId, 'resources'),
|
||||
reason: 'tournamentJoin',
|
||||
adjustments: [{ generalId: general.id, goldDelta: -develCost, minGoldAfter: 0 }],
|
||||
});
|
||||
@@ -511,6 +517,7 @@ export const tournamentRouter = router({
|
||||
} catch (error) {
|
||||
await ctx.turnDaemon.requestCommand({
|
||||
type: 'adjustGeneralResources',
|
||||
requestId: tournamentJoinCommandRequestId(ctx.requestId, 'projection-rollback-resources'),
|
||||
reason: 'tournamentJoinRollback',
|
||||
adjustments: [{ generalId: general.id, goldDelta: develCost }],
|
||||
});
|
||||
|
||||
@@ -277,15 +277,33 @@ describe('tournament router permissions and mutations', () => {
|
||||
nextAt: '2026-07-26T01:00:00.000Z',
|
||||
});
|
||||
await redis.set('sammo:che:default:tournament:participants', '[]');
|
||||
const caller = appRouter.createCaller(
|
||||
buildContext({ redis, transport, generals: [general], userId: 'user-1', develCost: 200 })
|
||||
);
|
||||
const context = buildContext({
|
||||
redis,
|
||||
transport,
|
||||
generals: [general],
|
||||
userId: 'user-1',
|
||||
develCost: 200,
|
||||
requestId: 'http:tournament-join',
|
||||
});
|
||||
const outerApiTransaction = vi.fn(async () => {
|
||||
throw new Error('tournament join must not hold an API transaction while waiting for the daemon');
|
||||
});
|
||||
Object.assign(context.db, { $transaction: outerApiTransaction });
|
||||
const caller = appRouter.createCaller(context);
|
||||
|
||||
await expect(caller.tournament.join()).resolves.toEqual({ ok: true, count: 1 });
|
||||
await expect(caller.tournament.join()).resolves.toEqual({ ok: true, count: 1 });
|
||||
|
||||
expect(transport.gold.get(general.id)).toBe(1_800);
|
||||
expect(transport.commands.filter((command) => command.type === 'adjustGeneralResources')).toHaveLength(1);
|
||||
expect(transport.commands).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'adjustGeneralResources',
|
||||
requestId: 'http:tournament-join:tournamentJoin:resources',
|
||||
reason: 'tournamentJoin',
|
||||
})
|
||||
);
|
||||
expect(outerApiTransaction).not.toHaveBeenCalled();
|
||||
expect(transport.commands.filter((command) => command.type === 'setMySetting')).toHaveLength(0);
|
||||
const snapshot = await caller.tournament.getSnapshot();
|
||||
expect(snapshot.participants).toHaveLength(1);
|
||||
|
||||
Reference in New Issue
Block a user