Skip to content

fix(router): buffer bodies when routing-key override is enabled - #2355

Merged
slin1237 merged 1 commit into
mainfrom
fix/routing-key-override-buffered-body
Aug 28, 2026
Merged

slin1237 merged 1 commit into
mainfrom
fix/routing-key-override-buffered-body

Conversation

@slin1237

Copy link
Copy Markdown
Member

Description

Problem

#2286 made the stream-vs-buffer decision per request. When --routing-key-override is enabled, a request carrying a valid routing-key header or tokens hint could qualify for the streamed pass-through, where the JSON body is never parsed. That silently changed the override's semantics:

  • body rid must win over the routing-key headers, but streamed requests never see it — two requests from the same rid lineage with different header keys can land on different workers
  • selection loses the body-derived inputs (rid, input_ids) it would have received on the buffered path

Before #2286, streaming was opt-in (--stream-request-bodies-over, default off), so enabling the override guaranteed every body was parsed and rid precedence always held.

Solution

Make an enabled routing-key override the first hard-buffer reason in the body-path decision. With the override on, every request takes the buffered typed path, restoring the pre-#2286 contract: body rid wins over header keys and body tokens stay visible to selection. No new flags; the decision is observable through the existing smg_router_request_body_path_total counter as path="buffered", reason="routing_key_override".

Changes

  • routers/common/body_policy.rs: add routing_key_override to BodyPathInputs and return Buffer("routing_key_override") ahead of every other reason
  • policies/registry.rs: add a routing_key_override_enabled() accessor
  • routers/http/router.rs: feed the flag into the decision and update the streamed-path comment
  • config/types.rs, main.rs: document the buffering contract on the flag
  • tests/common: build the harness policy registry with the config's routing-key override, matching the production builder (it previously dropped the override, so integration tests could not exercise it)
  • tests: reason-order unit test, a decision-level test covering no-hint / key-hint / tokens-hint on text-routing and load-based policies, and an end-to-end regression test

Test Plan

  • cargo +nightly fmt --all
  • cargo clippy --all-targets -- -D warnings
  • cargo test -p smg (unit + integration, all green)

The new integration test routing_key_override_buffers_and_prefers_body_rid reproduces the regression: retries disabled, a valid tokens hint, distinct per-request header keys, and a shared body rid lineage. Against the pre-fix code both requests stream and split across workers by header key; with this fix they buffer, pin to one worker via the stripped rid lineage, and the forwarded body keeps rid and input_ids intact.

Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@github-actions github-actions Bot added tests Test changes model-gateway Model gateway crate changes labels Aug 28, 2026
@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 81487a14-9511-4c66-a6b7-59b31253d86c

📥 Commits

Reviewing files that changed from the base of the PR and between 769881d and 875ef5d.

📒 Files selected for processing (7)
  • model_gateway/src/config/types.rs
  • model_gateway/src/main.rs
  • model_gateway/src/policies/registry.rs
  • model_gateway/src/routers/common/body_policy.rs
  • model_gateway/src/routers/http/router.rs
  • model_gateway/tests/common/mod.rs
  • model_gateway/tests/routing/stream_request_body_test.rs

Included review availability: 7 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 8 reviews per hour.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved sticky-session routing when routing keys are overridden.
    • Request bodies are buffered when needed so body-based request IDs take precedence over routing-key headers.
    • Streamed requests now correctly fall back to routing-key headers when no request ID is available.
  • Tests
    • Added coverage confirming consistent worker routing when body IDs differ from routing-key headers.

Walkthrough

Routing-key overrides now force automatically forwarded requests through buffered body routing. The body rid takes precedence over the routing-key header. Registry state, router selection, tests, and documentation reflect this behavior.

Changes

Routing-key override routing

Layer / File(s) Summary
Body policy precedence
model_gateway/src/policies/registry.rs, model_gateway/src/routers/common/body_policy.rs
The policy registry exposes whether routing-key overrides are enabled. Body-path selection buffers requests for this condition before other buffering reasons.
Router buffering integration
model_gateway/src/routers/http/router.rs, model_gateway/tests/common/mod.rs
The HTTP router passes override state into body-path selection. Streamed requests no longer use the routing-key override as a header fallback. Router tests cover hints, tokens, and policy combinations.
Integration validation and documentation
model_gateway/tests/routing/stream_request_body_test.rs, model_gateway/src/config/types.rs, model_gateway/src/main.rs
Integration tests verify body rid routing, buffering, Content-Length, and preserved JSON fields. Configuration and CLI documentation describe header fallback and buffered forwarding.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 875ef

The change restores buffered request-body routing when routing-key override is enabled so body-derived routing data and precedence are preserved; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant HTTPRequest
  participant request_body_path
  participant PolicyRegistry
  participant BodyPathPolicy
  participant Worker
  HTTPRequest->>request_body_path: evaluate request body path
  request_body_path->>PolicyRegistry: routing_key_override_enabled()
  PolicyRegistry-->>request_body_path: override enabled
  request_body_path->>BodyPathPolicy: select buffered path
  BodyPathPolicy-->>request_body_path: body RID remains available
  request_body_path->>Worker: forward buffered request
Loading

Suggested reviewers: catherinesue, key4ng

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: buffering request bodies when routing-key override is enabled.
Description check ✅ Passed The description directly explains the regression, the buffering solution, affected components, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 7 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/routing-key-override-buffered-body

Warning

Your free Security trial is over. An organization admin can activate Security or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

/// Whether sticky routing may derive its preferred key from the request
/// body's `rid`, requiring automatic body-path selection to keep the body
/// readable.
pub(crate) fn routing_key_override_enabled(&self) -> bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: This accessor reports "the override may read body rid" purely from enabled, but select_worker only consults the sticky map when routing_key_override_applies(policy.name()) is true — i.e. never for manual / consistent_hashing (line 346). With --routing-key-override + --policy manual, the override is a no-op for selection, yet every request now takes the buffered path for a rid nothing will consume. any_policy_needs_request_text already handles this asymmetry by folding routing_key_override_applies into its per-policy scan; mirroring it here (override enabled and at least one registered policy the override can apply to, since the model — hence the policy — is inside the unread body) would keep those deployments streamable.

Related: because decide_body_path now checks routing_key_override first, the keyed_override waiver at line 735 can only be true in exactly the case where the result is discarded (the sole production caller is Router::request_body_path). Its doc comment — "a valid routing-key header under the sticky override supersedes the policy before it reads text" — describes a streamed outcome that can no longer happen, so it's worth updating or dropping alongside the branch.

.worker_registry
.get_routing_pool(crate::worker::UNKNOWN_MODEL_ID, RoutingPool::HttpRegular);
decide_body_path(&BodyPathInputs {
routing_key_override: self.policy_registry.routing_key_override_enabled(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: All BodyPathInputs fields are evaluated eagerly, so with the override on every request still pays for get_routing_pool (clones the whole HTTP-regular pool into a Vec), the mutates_request() scan, any_policy_needs_request_text (scans the default policy plus every per-model policy, parses two hint headers), and model_count() — only to be told Buffer("routing_key_override") unconditionally. That's pure per-request waste on the ingress hot path for the entire class of deployments this PR targets. An early return before gathering the pool avoids it:

    fn request_body_path(&self, headers: &HeaderMap, wasm_request_hooks: bool) -> BodyPath {
        if self.policy_registry.routing_key_override_enabled() {
            return BodyPath::Buffer(REASON_ROUTING_KEY_OVERRIDE);
        }
        ...

(with routing_key_override: false left in the struct for the shared matrix, or the field dropped if the gate lives only here).

Comment on lines +351 to +354
let policy_registry = Arc::new(PolicyRegistry::with_override(
config.policy.clone(),
config.routing_key_override.clone(),
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: Good fix, but the same gap remains in the sibling harness: tests/common/test_app.rs:51 (create_test_app, which does take a router_config) still builds PolicyRegistry::new(router_config.policy.clone()) and silently drops routing_key_override. Any future test that wires the override through that entry point will pass for the wrong reason. Worth converting it to with_override too so both harnesses match the production builder. (test_app.rs:245 takes no config, so it's unaffected in practice.)

@slin1237
slin1237 merged commit 12be9dc into main Aug 28, 2026
17 of 21 checks passed
@slin1237
slin1237 deleted the fix/routing-key-override-buffered-body branch August 28, 2026 19:04
hello-alexmcc added a commit that referenced this pull request Sep 8, 2026
Three merged router fixes changed behaviour a client can observe without
leaving an end-to-end check behind:

- #2417: an overload shed must answer 503 with its own
  worker_overload_protection_shed code and a Retry-After, not the generic
  no_available_workers. The engine is pinned to one running request so a
  burst piles up in its queue; a probe sent while the queue is deep must be
  shed, and the worker must serve again once the burst drains.
- #2355: with --routing-key-override a body rid must outrank the routing
  key header. Two workers under the manual policy: a header key stays
  sticky, and a request whose header names one lineage and whose body rid
  names another lands on the rid's worker. The serving worker is observed
  through the gateway's in-flight load during a long generation.
- #2427: a gRPC worker stopped under a live gateway must be reported
  unhealthy, requests must fail fast rather than hang, and after the
  worker restarts on the same port it must return to rotation. The test
  borrows the session pool's worker so it does not race a cached worker
  for the GPU.

Signed-off-by: Alex McC <319643551+hello-alexmcc@users.noreply.github.com>
hello-alexmcc added a commit that referenced this pull request Sep 8, 2026
Three merged router fixes changed behaviour a client can observe without
leaving an end-to-end check behind:

- #2417: an overload shed must answer 503 with its own
  worker_overload_protection_shed code and a Retry-After, not the generic
  no_available_workers. The engine is pinned to one running request so a
  burst piles up in its queue; a probe sent while the queue is deep must be
  shed, and the worker must serve again once the burst drains.
- #2355: with --routing-key-override a body rid must outrank the routing
  key header. Two workers under the manual policy: a header key stays
  sticky, and a request whose header names one lineage and whose body rid
  names another lands on the rid's worker. The serving worker is observed
  through the gateway's in-flight load during a long generation.
- #2427: a gRPC worker stopped under a live gateway must be reported
  unhealthy, requests must fail fast rather than hang, and after the
  worker restarts on the same port it must return to rotation. The test
  borrows the session pool's worker so it does not race a cached worker
  for the GPU.

Signed-off-by: Alex McC <319643551+hello-alexmcc@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model-gateway Model gateway crate changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant