Skip to content

Commit 1c4e9f1

Browse files
committed
fix test
1 parent 7f543a8 commit 1c4e9f1

1 file changed

Lines changed: 51 additions & 91 deletions

File tree

test/src/concurrent-set-state.test.ts

Lines changed: 51 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -148,105 +148,65 @@ describe("concurrent setState", () => {
148148
);
149149
});
150150

151-
it("should expose lost update when stale write commits last", async () => {
152-
await Match.create(match);
153-
154-
const stateA: State = {
155-
...state,
156-
ctx: { ...state.ctx, currentPlayer: "102", turn: 2 },
157-
_stateID: 2,
158-
};
159-
const stateB: State = {
160-
...state,
161-
ctx: { ...state.ctx, currentPlayer: "103", turn: 3 },
162-
_stateID: 3,
163-
};
164-
165-
const logA: LogEntry[] = [
166-
{
167-
...logEntry,
151+
// Regression: without SELECT … FOR UPDATE, concurrent setState calls can
152+
// read the same stale _stateID, both pass the < check, and the last writer
153+
// wins — even if it carries a lower _stateID (lost update).
154+
// The row lock ensures the second transaction sees the first's committed
155+
// write, so the stale update is correctly rejected.
156+
// We repeat the race multiple times to catch non-deterministic regressions.
157+
it("should never regress to a lower _stateID under concurrent writes", async () => {
158+
const ITERATIONS = 20;
159+
160+
for (let i = 0; i < ITERATIONS; i++) {
161+
await testStore.beforeEach(); // reset DB between iterations
162+
163+
await Match.create(match);
164+
165+
const stateA: State = {
166+
...state,
167+
ctx: { ...state.ctx, currentPlayer: "102", turn: 2 },
168168
_stateID: 2,
169-
turn: 2,
170-
action: {
171-
...logEntry.action,
172-
payload: { ...logEntry.action.payload, playerID: "102" },
173-
},
174-
},
175-
];
176-
const logB: LogEntry[] = [
177-
{
178-
...logEntry,
169+
};
170+
const stateB: State = {
171+
...state,
172+
ctx: { ...state.ctx, currentPlayer: "103", turn: 3 },
179173
_stateID: 3,
180-
turn: 3,
181-
action: {
182-
...logEntry.action,
183-
payload: { ...logEntry.action.payload, playerID: "103" },
174+
};
175+
176+
const logA: LogEntry[] = [
177+
{
178+
...logEntry,
179+
_stateID: 2,
180+
turn: 2,
181+
action: {
182+
...logEntry.action,
183+
payload: { ...logEntry.action.payload, playerID: "102" },
184+
},
184185
},
185-
},
186-
];
187-
188-
const originalFindByPk = Match.findByPk.bind(Match);
189-
const originalUpsert = Match.upsert.bind(Match);
190-
191-
let findByPkCount = 0;
192-
let releaseReads!: () => void;
193-
const readsReady = new Promise<void>((resolve) => {
194-
releaseReads = resolve;
195-
});
196-
197-
let releaseStateAWrite!: () => void;
198-
const stateAWriteGate = new Promise<void>((resolve) => {
199-
releaseStateAWrite = resolve;
200-
});
201-
202-
const findByPkSpy = jest
203-
.spyOn(Match, "findByPk")
204-
.mockImplementation(async (...args: Parameters<typeof Match.findByPk>) => {
205-
const row = await originalFindByPk(...args);
206-
findByPkCount += 1;
207-
if (findByPkCount === 2) {
208-
releaseReads();
209-
}
210-
await readsReady;
211-
return row;
212-
});
213-
214-
const upsertSpy = jest
215-
.spyOn(Match, "upsert")
216-
.mockImplementation(async (...args: Parameters<typeof Match.upsert>) => {
217-
const values = args[0] as { state?: State };
218-
const incomingStateID = values.state?._stateID;
219-
220-
if (incomingStateID === 2) {
221-
await stateAWriteGate;
222-
}
223-
224-
const result = await originalUpsert(...args);
225-
226-
if (incomingStateID === 3) {
227-
releaseStateAWrite();
228-
}
229-
230-
return result;
231-
});
186+
];
187+
const logB: LogEntry[] = [
188+
{
189+
...logEntry,
190+
_stateID: 3,
191+
turn: 3,
192+
action: {
193+
...logEntry.action,
194+
payload: { ...logEntry.action.payload, playerID: "103" },
195+
},
196+
},
197+
];
232198

233-
try {
234199
await Promise.all([
235200
testStore.db.setState(match.id!, stateA, logA),
236201
testStore.db.setState(match.id!, stateB, logB),
237202
]);
238-
} finally {
239-
findByPkSpy.mockRestore();
240-
upsertSpy.mockRestore();
241-
}
242203

243-
const result = await testStore.db.fetch(match.id!, {
244-
state: true,
245-
log: true,
246-
});
204+
const result = await testStore.db.fetch(match.id!, {
205+
state: true,
206+
});
247207

248-
// Correct behavior would keep the highest _stateID even if stale write runs last.
249-
// This expectation is intentionally red until setState is fixed.
250-
expect(result.state!._stateID).toBe(3);
251-
});
208+
expect(result.state!._stateID).toBe(3);
209+
expect(result.state!.ctx.currentPlayer).toBe("103");
210+
}
211+
}, 30_000);
252212
});

0 commit comments

Comments
 (0)