Repository navigation
fix(mcp-router): make ExtProcServer.RoutingConfig swap race-free - #925
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR changes ChangesAtomic RoutingConfig Concurrency Fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
28b5fbd to
ed62bbe
Compare
|
Rebased onto main against the SSRF/backend-init JWT changes that landed in this file. Reader sites are down to four now (one path moved off |
|
Hey, thanks for the contribution. Just a heads up: reviews are currently being prioritised around the v0.7 milestone. PRs tied to v0.7 issues will be reviewed first. We'll get to this one, but it may take a bit longer than usual. |
Patryk-Stefanski
left a comment
There was a problem hiding this comment.
Good race fix. atomic.Pointer is the right tool here — channels would add latency on the hot path, and this is a classic read-heavy config-swap pattern.
Blocker: missed RoutingConfig access site
response_handlers.go:62 still reads s.RoutingConfig directly:
serverInfo, cfgErr := s.RoutingConfig.GetServerConfigByName(req.serverName)This needs s.RoutingConfig.Load().GetServerConfigByName(...) too — same race window as the request handler sites you already fixed.
Nit: snapshot capture in initializeMCPSeverSession
The PR body mentions capturing the config snapshot once to avoid torn reads. Verify that both GetServerConfigByName (line ~675) and MCPGatewayInternalHostname (line ~756) use the same snapshot variable rather than calling .Load() twice.
Missing trailing newlines
Per project style, files must end with a newline.
The overall approach is solid — just needs that one missed site in response_handlers.go.
jasonmadigan
left a comment
There was a problem hiding this comment.
needs a rebase -- merge conflicts with main.
ed62bbe to
71b95b9
Compare
|
Thanks @Patryk-Stefanski + @jasonmadigan . Pushed |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/mcp-router/server_test.go`:
- Around line 797-832: The test currently uses time.Sleep(200ms) to try to
overlap goroutines but doesn't guarantee either goroutine executed; replace the
sleep with a start barrier (e.g., sync.WaitGroup or a start channel) to ensure
both goroutines begin at the same time and use a bounded loop count (fixed
number of iterations) in each goroutine instead of an infinite loop so they
actually perform concurrent calls to server.HandleRequestHeaders and
server.OnConfigChange (which exercise RoutingConfig.Load/Store) before closing
the stop signal; also remove the now-unused time import. Ensure you reference
the existing goroutine bodies that call
server.HandleRequestHeaders(context.Background(), nil) and
server.OnConfigChange(context.Background(), &config.MCPServersConfig{...}) and
change their loops to wait on the shared start barrier, run N iterations, then
return so wg.Wait() deterministically observes concurrent access.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e262f86e-d893-4db0-aaab-8c7f3da54320
📒 Files selected for processing (7)
cmd/mcp-broker-router/router.gointernal/mcp-router/request_handlers.gointernal/mcp-router/request_handlers_test.gointernal/mcp-router/response_handlers.gointernal/mcp-router/response_handlers_test.gointernal/mcp-router/server.gointernal/mcp-router/server_test.go
5db7433 to
f558656
Compare
|
Hey |
f558656 to
985db05
Compare
jasonmadigan
left a comment
There was a problem hiding this comment.
rebased onto main, all conflicts resolved. tests pass, race detector clean.
atomic.Pointer is the right call for this read-heavy pattern. snapshot in initializeMCPServerSession prevents torn reads. race test is deterministic (channel barrier + 5000 iterations, no time.Sleep).
nits (author's call):
- router.go constructor comment is verbose for project style
- server.go RoutingConfig field comment is wordy
- race test uses blank _ for *testing.T
OnConfigChange fires from the viper fsnotify goroutine and did a single bare assignment to s.RoutingConfig, while Process is invoked once per concurrent ext_proc gRPC stream and its handlers read RoutingConfig on every request. The pointer write is not atomic in Go and the race detector fires; a config reload landing during a request can leave a goroutine reading a stale MCPServersConfig under the new pointer. Convert ExtProcServer.RoutingConfig to atomic.Pointer[config.MCPServersConfig]. OnConfigChange becomes Store, and every reader site loads through .Load(). Seed the initial pointer in createRouter (cmd/mcp-broker-router/router.go) so Load is non-nil before the first OnConfigChange fires, and panic with a clear message if mcpConfig is unexpectedly nil at construction time. Address review on the rebased branch: - response_handlers.go: the 401 elicitation path was added to main after this PR was authored. Route it through s.RoutingConfig.Load() like the other handler sites so it builds against the atomic.Pointer field. - request_handlers.go: capture the routing config snapshot once at the top of initializeMCPServerSession and reuse the local for both GetServerConfigByName and MCPGatewayInternalHostname. Avoids torn reads if a Store lands between the two calls. Update test fixtures across server_test.go, request_handlers_test.go, and response_handlers_test.go to use the atomic.Pointer accessor. Add TestExtProcServer_OnConfigChange_DataRace exercising HandleRequestHeaders concurrently with OnConfigChange. Run with go test -race ./internal/mcp-router/... Rebased onto upstream/main e99c0a6. Same lock-discipline class as the recent merged concurrency fixes (Kuadrant#860, Kuadrant#872, Kuadrant#888, Kuadrant#911, Kuadrant#918, Kuadrant#921, Kuadrant#922). Signed-off-by: SarthakB11 <[email protected]>
Address CodeRabbit review on the new TestExtProcServer_OnConfigChange_DataRace. The original used time.Sleep(200ms) to overlap goroutines, which is probabilistic; one goroutine could exit early without exercising RoutingConfig.Load/Store at all. Replace with: - a start channel as a wait barrier so both goroutines begin simultaneously - bounded 5000-iteration loops in each goroutine for a known concurrent workload that the race detector can sample against - close(start) + wg.Wait() for deterministic termination Removes the now-unused time import. Verified: go test -race -run TestExtProcServer_OnConfigChange_DataRace ./internal/mcp-router/ runs in ~1.1s with the race detector clean. Signed-off-by: SarthakB11 <[email protected]>
985db05 to
f22cbdd
Compare
|
thanks @SarthakB11 |
|
Thanks for the rebase and the architecture sign-off, jasonmadigan. Happy to push the three nit cleanups if useful, otherwise good to merge as-is. |
Address jasonmadigan review feedback: - router.go: shorten constructor seed-config comment - server.go: condense RoutingConfig field comment to one line - server_test.go: name *testing.T param t instead of _ Signed-off-by: SarthakB11 <[email protected]>
Head branch was pushed to by a user without write access
|
Pushed 58297fd with the 3 nit cleanups: router.go constructor comment trimmed, server.go RoutingConfig field comment shortened, race test uses named t instead of _. CI green locally (build + tests + race + vet). |
The prior nit cleanup renamed _ *testing.T to t *testing.T but left the parameter unused, which golangci-lint (revive) flags. Capture HandleRequestHeaders errors in the reader goroutine and surface them via t.Errorf so the parameter is genuinely used and a real handler error during the race exercise no longer goes silent. Signed-off-by: SarthakB11 <[email protected]>
|
Pushed The conformance-tests failure on the same run looks unrelated to this branch (conformance-server |
Summary
Same lock-discipline class as the recent broker race fixes (#860, #872, #888, #918, #921, #911), this one on the router side.
ExtProcServer.OnConfigChangeininternal/mcp-router/server.gomutatess.RoutingConfigwith a single bare assignment. It is registered as aconfig.Observerand fires from viper's fsnotify-driven goroutine, whileProcessis invoked once per concurrent ext_proc gRPC stream and the handlers it dispatches reads.RoutingConfigfrom at least five sites inrequest_handlers.go(MCPGatewayExternalHostname,GetServerConfigByName,MCPGatewayInternalHostname,RouterAPIKey). The pointer write is not atomic in Go, the race detector fires, and a config reload that lands during atools/callcan leave a goroutine reading a staleMCPServersConfigunder the new pointer. In practice that surfaces as wrong-server routing, or 401 ifRouterAPIKeyrotated.Convert
ExtProcServer.RoutingConfigtoatomic.Pointer[config.MCPServersConfig].OnConfigChangebecomesStore, readers becomeLoad.initializeMCPSeverSessionreads bothMCPGatewayInternalHostnameandRouterAPIKeyfor the same logical operation, so it captures the snapshot once viaroutingConfig := s.RoutingConfig.Load(). That keeps the host and the API key from the same configuration generation rather than re-Loading twice and risking a torn read across a reload.setUpRouterseeds the initial pointer soLoad()is non-nil before the firstOnConfigChangefires. It also panics with a clear message ifmcpConfigis unexpectedly nil at construction time, which is a new failure mode but a loud one (better than a deferred nil deref deep in a hot path).Test fixtures in
server_test.goandrequest_handlers_test.goupdated to use theatomic.Pointeraccessor.Verification
Summary by CodeRabbit
Bug Fixes
:authorityrewriting and backend target/server resolution during token elicitation.Tests