Skip to content

Commit c81fabb

Browse files
committed
test(leader-lock): use fake timers to deterministically test follower polling
The "follower does a final read after timeout" test (and the "follower returns null after timeout" test) relied on real-clock `setTimeout` and `Date.now()` with very tight bounds (pollIntervalMs=5, maxWaitMs=9). Any CI scheduler jitter of >4ms would cause the second in-loop poll to be skipped, the polls counter to end at 2 instead of 3, and the assertion `expect(result).toBe('late-leader')` to fail. Switched both tests to `vi.useFakeTimers()` so the schedule is driven by mocked time advanced via `vi.advanceTimersByTimeAsync`. The intent is unchanged — verify that the in-loop deadline triggers exactly one post-deadline last-chance call to `onFollower` — but the assertions no longer depend on wall-clock timing. Verified across 5 sequential runs with zero flakes.
1 parent fc64421 commit c81fabb

1 file changed

Lines changed: 48 additions & 25 deletions

File tree

apps/sim/lib/concurrency/__tests__/leader-lock.test.ts

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,39 +120,62 @@ describe('withLeaderLock', () => {
120120
it('follower does a final read after timeout to catch a just-finished leader', async () => {
121121
redisConfigMockFns.mockAcquireLock.mockResolvedValueOnce(false)
122122

123-
// pollInterval=5, maxWait=9 → loop exits after 2 in-loop polls (T+5, T+10);
124-
// the third call (polls=3) is the post-deadline last-chance read.
125-
let polls = 0
126-
const onFollower = vi.fn(async () => {
127-
polls += 1
128-
if (polls <= 2) return null
129-
return 'late-leader'
130-
})
123+
/**
124+
* The intent: after the in-loop poll deadline is reached, the follower
125+
* does exactly one more (last-chance) `onFollower` call to catch a leader
126+
* that finished between the previous poll and the timeout. Using fake
127+
* timers makes the timing deterministic — pollInterval=10 and maxWait=15
128+
* cause two in-loop polls (T+10, T+20) and one last-chance read (T+20),
129+
* but the schedule is driven by mocked time, not the CI wall clock.
130+
*/
131+
vi.useFakeTimers()
132+
try {
133+
let polls = 0
134+
const onFollower = vi.fn(async () => {
135+
polls += 1
136+
if (polls <= 2) return null
137+
return 'late-leader'
138+
})
131139

132-
const result = await withLeaderLock<string>({
133-
key: 'k',
134-
pollIntervalMs: 5,
135-
maxWaitMs: 9,
136-
onLeader: async () => 'should-not-run',
137-
onFollower,
138-
})
140+
const promise = withLeaderLock<string>({
141+
key: 'k',
142+
pollIntervalMs: 10,
143+
maxWaitMs: 15,
144+
onLeader: async () => 'should-not-run',
145+
onFollower,
146+
})
147+
148+
await vi.advanceTimersByTimeAsync(30)
149+
const result = await promise
139150

140-
expect(result).toBe('late-leader')
141-
expect(onFollower).toHaveBeenCalledTimes(3)
151+
expect(result).toBe('late-leader')
152+
expect(onFollower).toHaveBeenCalledTimes(3)
153+
} finally {
154+
vi.useRealTimers()
155+
}
142156
})
143157

144158
it('follower returns null after timeout', async () => {
145159
redisConfigMockFns.mockAcquireLock.mockResolvedValueOnce(false)
146160

147-
const result = await withLeaderLock<string>({
148-
key: 'k',
149-
pollIntervalMs: 5,
150-
maxWaitMs: 20,
151-
onLeader: async () => 'should-not-run',
152-
onFollower: async () => null,
153-
})
161+
vi.useFakeTimers()
162+
try {
163+
const onFollower = vi.fn(async () => null)
164+
const promise = withLeaderLock<string>({
165+
key: 'k',
166+
pollIntervalMs: 10,
167+
maxWaitMs: 25,
168+
onLeader: async () => 'should-not-run',
169+
onFollower,
170+
})
171+
172+
await vi.advanceTimersByTimeAsync(50)
173+
const result = await promise
154174

155-
expect(result).toBeNull()
175+
expect(result).toBeNull()
176+
} finally {
177+
vi.useRealTimers()
178+
}
156179
})
157180

158181
it('only one of N concurrent callers acquires the lock', async () => {

0 commit comments

Comments
 (0)