Skip to content

Commit 118eb9b

Browse files
authored
fix: Release Unassigned Workspace Slots When Dispatch Cleanup Fails (LibreChat-AI#189)
Closes LibreChat-AI#170
1 parent 31def17 commit 118eb9b

2 files changed

Lines changed: 69 additions & 11 deletions

File tree

‎service/src/bridge/concurrent-store.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,46 @@ test('queued cancellation never leases and does not block another root', async (
384384
).toBeUndefined();
385385
});
386386

387+
test('unassigned slot releases even when dispatch cleanup fails', async () => {
388+
await register();
389+
const originalIncr = redis.incr.bind(redis);
390+
const originalSet = redis.set.bind(redis);
391+
const set = originalSet as (...args: unknown[]) => unknown;
392+
redis.incr = ((key: string) =>
393+
key.endsWith(':generation')
394+
? Promise.reject(new Error('injected generation outage'))
395+
: originalIncr(key)) as typeof redis.incr;
396+
redis.set = ((key: string, ...args: unknown[]) =>
397+
key.endsWith(':cancelled')
398+
? Promise.reject(new Error('injected cancellation outage'))
399+
: set(key, ...args)) as typeof redis.set;
400+
try {
401+
// The reservation succeeds, then dispatch fails before storing an assignment.
402+
await expect(dispatch('a')).rejects.toThrow('injected cancellation outage');
403+
} finally {
404+
redis.incr = originalIncr;
405+
redis.set = originalSet;
406+
}
407+
expect(
408+
await redis.hlen(`codeapi:bridge:v1:worker:${workerId}:workspace-slots`),
409+
).toBe(0);
410+
expect(
411+
await redis.get(`codeapi:bridge:v1:worker:${workerId}:lock`),
412+
).toBeNull();
413+
const next = dispatch('a');
414+
const assignment = (await store.lease(
415+
workerId,
416+
incarnationId,
417+
1000,
418+
undefined,
419+
undefined,
420+
0,
421+
))!;
422+
expect(assignment.request).toMatchObject({ workspaceId: 'a' });
423+
await settle(assignment);
424+
await expect(next).resolves.toMatchObject({ status: 'rejected' });
425+
});
426+
387427
test('late quarantine releases its slot after caller cancellation and retains only its root fence', async () => {
388428
await register();
389429
const controller = new AbortController();

‎service/src/bridge/store.ts‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,20 +1060,15 @@ export class RedisBridgeStore {
10601060
// already committed result rather than turning cleanup availability
10611061
// into a client-visible failure that could prompt duplicate work.
10621062
}
1063+
} else if (workspaceSlots != null && assignment == null) {
1064+
await this.cleanupUnassignedSlot(
1065+
args.workerId,
1066+
lockIncarnationId,
1067+
assignmentId,
1068+
);
10631069
} else {
10641070
await this.cleanupDispatch(args.workerId, assignmentId, assignment);
10651071
}
1066-
if (workspaceSlots != null && assignment == null) {
1067-
await boundedCommand(
1068-
workspaceSlots.release(
1069-
args.workerId,
1070-
lockIncarnationId,
1071-
assignmentId,
1072-
),
1073-
this.redisCommandTimeoutMs,
1074-
'Bridge unassigned slot cleanup',
1075-
);
1076-
}
10771072
}
10781073
}
10791074

@@ -2138,6 +2133,29 @@ export class RedisBridgeStore {
21382133
]);
21392134
}
21402135

2136+
private async cleanupUnassignedSlot(
2137+
workerId: string,
2138+
incarnationId: string,
2139+
assignmentId: string,
2140+
): Promise<void> {
2141+
// No stored assignment owns this reservation, so a cancellation outage
2142+
// must not leave the slot and its root busy until TTL expiry.
2143+
const [cleanup, release] = await Promise.allSettled([
2144+
this.cleanupDispatch(workerId, assignmentId, undefined),
2145+
boundedCommand(
2146+
new BridgeWorkspaceSlots(this.redis).release(
2147+
workerId,
2148+
incarnationId,
2149+
assignmentId,
2150+
),
2151+
this.redisCommandTimeoutMs,
2152+
'Bridge unassigned slot cleanup',
2153+
),
2154+
]);
2155+
if (cleanup.status === 'rejected') throw cleanup.reason;
2156+
if (release.status === 'rejected') throw release.reason;
2157+
}
2158+
21412159
private async commitPendingWorkspace(
21422160
assignment: StoredAssignment,
21432161
settlement: AnyCodeBridgeSettlement,

0 commit comments

Comments
 (0)