Skip to content

Commit 395a394

Browse files
authored
fix(test): assert recovery backoff on the claims, not worker request arrival (#9591)
1 parent 78cd332 commit 395a394

1 file changed

Lines changed: 30 additions & 3 deletions

File tree

‎apps/sim/lib/mothership/request/session/recovery-storm.integration.ts‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const { redisUrl, inheritedEnv, worker } = await vi.hoisted(async () => {
2626
}
2727
const worker = {
2828
runs: new Map<string, RunFixture>(),
29+
/** The backoff each successful takeover persisted, in claim order, by run id. */
30+
claims: new Map<string, { attempts: number; claimedAt: number; notBefore: number }[]>(),
2931
unexpectedRequests: [] as { path: string; reason: string }[],
3032
}
3133
const server = createServer(async (request, response) => {
@@ -88,6 +90,19 @@ const { redisUrl, inheritedEnv, worker } = await vi.hoisted(async () => {
8890
})
8991

9092
vi.mock('@/lib/auth', () => authMock)
93+
vi.mock('@/lib/mothership/request/lifecycle/controller-ownership', async (importOriginal) => {
94+
const actual =
95+
await importOriginal<typeof import('@/lib/mothership/request/lifecycle/controller-ownership')>()
96+
return {
97+
...actual,
98+
claimRunController: async (...args: Parameters<typeof actual.claimRunController>) => {
99+
const claimed = await actual.claimRunController(...args)
100+
const [{ runId, recoveryBackoff }] = args
101+
if (claimed) worker.claims.set(runId, [...(worker.claims.get(runId) ?? []), recoveryBackoff])
102+
return claimed
103+
},
104+
}
105+
})
91106
vi.mock('@/lib/mothership/request/session/buffer', async (importOriginal) => {
92107
const actual = await importOriginal<typeof import('@/lib/mothership/request/session/buffer')>()
93108
const { getRedisClient } = await import('@/lib/core/config/redis')
@@ -330,6 +345,7 @@ async function reconnect(
330345
afterAll(async () => {
331346
await new Promise<void>((resolve) => worker.server.close(() => resolve()))
332347
worker.runs.clear()
348+
worker.claims.clear()
333349
for (const [key, value] of Object.entries(inheritedEnv)) {
334350
if (value === undefined) delete process.env[key]
335351
else process.env[key] = value
@@ -428,9 +444,20 @@ describe.runIf(Boolean(redisUrl))('reconnecting to an orphaned Chat run', () =>
428444
const { turns, stops } = await reconnect(run, { tails, windowMs: 60_000 })
429445

430446
expect(turns).toHaveLength(MAX_RECOVERY_ATTEMPTS)
431-
/** Each takeover waits at least the jittered floor of the one before it. */
432-
turns.slice(1).forEach((turn, i) => {
433-
expect(turn.at - turns[i].at).toBeGreaterThanOrEqual(0.7 * 1_000 * 2 ** i)
447+
/**
448+
* Each takeover is planned no earlier than the jittered floor of the one before it,
449+
* read from the backoff each claim persisted. Worker request arrival would add the
450+
* variable work between a claim and its POST, which concurrent cases stretch.
451+
*/
452+
const claims = worker.claims.get(run.runId) ?? []
453+
expect(claims.map((claim) => claim.attempts)).toEqual(
454+
Array.from({ length: MAX_RECOVERY_ATTEMPTS + 1 }, (_, i) => i + 1)
455+
)
456+
claims.slice(1).forEach((claim, i) => {
457+
expect(claim.claimedAt).toBeGreaterThanOrEqual(claims[i].notBefore)
458+
expect(claims[i].notBefore - claims[i].claimedAt).toBeGreaterThanOrEqual(
459+
0.8 * 1_000 * 2 ** i
460+
)
434461
})
435462
expect(stops).toHaveLength(1)
436463
expect(await storedRun(run.runId)).toMatchObject({

0 commit comments

Comments
 (0)