Skip to content

fix: replace dedup resolver closure chain with array to prevent hangs#25

Merged
1bcMax merged 1 commit intoBlockRunAI:mainfrom
justiniggy:fix/dedup-resolver-race-condition
Feb 14, 2026
Merged

fix: replace dedup resolver closure chain with array to prevent hangs#25
1bcMax merged 1 commit intoBlockRunAI:mainfrom
justiniggy:fix/dedup-resolver-race-condition

Conversation

@justiniggy
Copy link
Contributor

Summary

  • Replace fragile closure-chain pattern in RequestDeduplicator with a simple resolvers array
  • Fix removeInflight() silently dropping waiters — they now receive a 503 response instead of hanging forever

Problem

1. Closure chain is fragile and hard to reason about:

The old getInflight() chained each new waiter by capturing and wrapping the previous entry.resolve via closures:

const orig = entry.resolve;
entry.resolve = (result) => {
  orig(result);    // call previous
  resolve(result); // resolve this waiter
};

This pattern is hard to verify for correctness and makes the code unnecessarily complex.

2. removeInflight() causes waiters to hang forever (real bug):

When the original request fails (client disconnect, timeout, error), removeInflight() was called which simply deleted the inflight entry:

removeInflight(key: string): void {
  this.inflight.delete(key);  // waiters' promises never resolve!
}

Any waiters (getInflight() callers) would have promises that never resolve, causing those HTTP responses to hang indefinitely until the client times out.

Fix

  • InflightEntry now holds resolvers: Array<(result) => void> instead of a single resolve function + waiters array
  • complete() iterates all resolvers
  • removeInflight() resolves all waiters with a 503 error response so they can retry independently

Test plan

  • Verify build passes: npm run build
  • Send two identical requests concurrently → second should wait for first and get same response
  • Kill the first request mid-flight → second should get 503 (not hang forever)
  • Send request, let it complete, send same request within 30s → should get cached response

🤖 Generated with Claude Code

The previous implementation chained waiters by patching entry.resolve
via closures. This was fragile and hard to reason about.

More critically, removeInflight() (called on client disconnect or
request error) deleted the inflight entry without resolving pending
waiters — causing them to hang forever with no response.

Changes:
- Replace closure-chain pattern with a simple resolvers array
- On complete(): iterate all resolvers and resolve each one
- On removeInflight(): resolve waiters with 503 error instead of
  leaving them hanging, so clients can retry independently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@1bcMax 1bcMax merged commit b25a348 into BlockRunAI:main Feb 14, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants