coderouter: wait for sticky credential refreshes on a lease-completion signal - #15087
Merged
Merged
Conversation
…l and fake clock Sticky sessions that hit an in-flight credential refresh must wake on the lease layer's refresh-completion signal, fall back to bounded lease re-reads through an injected clock when another instance holds the lease, give up at the patience deadline, and stop at once when the caller aborts. Refs #11308 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replace the codex plane's fixed 4 x 500 ms sleep-and-retry with a lease-layer wait. The repository wakes in-process waiters whenever it clears a refresh lease (complete, release, fail, expiry sweep). Refreshes held by another serverless instance are detected by re-reading the lease row with a 100 ms to 500 ms backoff through an injected, cancellable clock. The wait stays bounded at 2 s and 4 settle cycles, and the request's AbortSignal cancels it. Non-sticky requests still fail fast. Closes #11308 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
|
All contributors have signed the CLA ✍️ ✅ |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (8)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
|
Merge receipt for |
rustybret
pushed a commit
to rustybret/bmux
that referenced
this pull request
Sep 28, 2026
2604935 coderouter: wait for sticky credential refreshes on a lease-completion signal (manaflow-ai#15087) 369cd16 coderouter: scope org API keys to team-shared accounts (manaflow-ai#15086) 847c919 Keep non-ASCII startup input as UTF-8 (manaflow-ai#15081) a616a2b Remove obsolete bash PR watcher loops (manaflow-ai#15075) # Conflicts: # .github/workflows/build-ghosttykit.yml # .github/workflows/ci-guards.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11308
Summary
A sticky Codex session that hits a credential refresh already in flight used to sleep 500 ms and retry, up to 4 times. The delay had no relation to when the refresh finished: a refresh that finished in 50 ms still cost 500 ms, and each retry re-ran the full credential path (envelope read, decrypt, lease claim write). The session now waits on a refresh-completion signal from the lease layer and retries once the lease clears.
The lease lives in Postgres and the web app runs on Vercel serverless, so the refresh winner may be on this instance or on another one. The wait (
web/services/coderouter/refreshSignal.ts) therefore races two sources:completeRefreshLease,releaseRefreshLease,failRefreshLease, and the expired-lease sweep inrepository.tscallrefreshCompletionRegistry.settled(accountId)after they clear a lease. Local waiters wake with no delay and no extra query.refreshLeaseActive: unexpiredrefresh_lease_idpresent) after 100, 200, 400, then 500 ms steps. The delays go through an injectedRefreshWaitClock, which production backs with timers and tests replace with a fake. The request's AbortSignal cancels the sleep, the query, and the registry waiter.The whole wait is capped at 2 s and 4 settle-then-busy-again cycles, which matches the old maximum. At the cap, the last
CodeRouterRefreshBusyis rethrown and the proxy moves the session exactly as before. A failed lease re-read counts as "still held", so a database error can only end in the old move, never in a new error. Non-sticky requests still fail fast and never wait.Why not LISTEN/NOTIFY. The app connects through a transaction-mode pooler (
postgres(url, { prepare: false })), which does not keep LISTEN registrations across transactions. A dedicated direct listener connection per serverless instance would cost a database connection for each warm instance to save well under a second in a rare race. The trade-off of the chosen design: a refresh on another instance is detected up to one backoff step late (at most 500 ms), and each waiting sticky request issues at most about 6 cheap primary-key reads.Claude plane. On current main,
claudeProxy.tsuses static upstream secrets with no refresh lease and no sleep, so there is nothing to change there. The Claude refresh path with the same fixed delay lives only on the open #11283.createStickyRefreshPatienceis plane-agnostic, and that PR should adopt the exportedstickyRefreshPatienceso both planes behave the same.No
setTimeoutremains incodexProxy.tsorclaudeProxy.tsfor this path. The only timer is the production clock implementation inrefreshSignal.ts.Testing
Commit 1 (00874e3) changes the tests only and fails:
bun test tests/coderouter-refresh-signal.test.ts tests/coderouter-responses-proxy.test.tsreportsCannot find module '../services/coderouter/refreshSignal'. Commit 2 (f956975) adds the implementation.From
web/on f956975:bun test tests/coderouter-*: 461 pass, 41 skip (DB-gated), 0 fail.AbortErrorwith no credential retry, no re-read, and no pending sleep; a non-sticky request never waits.CMUX_DB_TEST=1 bun test tests/coderouter-routing-db-behavior.test.tsagainst a throwaway local Postgres 17 (schema fromdrizzle-kit push): the 2 new lease tests pass (clearing a lease through release and fail wakes the registry and flipsrefreshLeaseActive; an expired lease reads as not active). 3 route-token VM binding tests in that file fail identically on unmodifiedorigin/main, so they are unrelated to this change.bun run typecheck: pass.bun run lint:complexity: pass, 42 findings matched the baseline, none new.Not verified: behavior against the live PlanetScale pooler or on Vercel. No production or staging database was touched.
Changelog
none
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Fixes #11308. Sticky Codex sessions that hit an in-flight credential refresh now wait on a refresh-completion signal from the lease layer instead of sleeping a fixed 500 ms and retrying up to 4 times.
Behavior
refreshSignal.ts.claudeProxy.tsis unchanged —stickyRefreshPatienceis an exported seam that the Claude refresh path should adopt so both planes behave the same.Written for commit f956975. Summary will update on new commits.
Summary by CodeRabbit