Skip to content

fix(router): distinguish overload from unavailable [DYN-3279] - #10941

Merged
PeaBrane merged 2 commits into
mainfrom
codex/overload-529
Jun 25, 2026
Merged

PeaBrane merged 2 commits into
mainfrom
codex/overload-529

Conversation

@PeaBrane

@PeaBrane PeaBrane commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Overview:

Distinguish temporary capacity overload from genuine backend unavailability at the HTTP boundary.

Summary

  • Return HTTP 529 for worker overload and queue-capacity rejection.
  • Return HTTP 503 when no backend worker can serve the request.
  • Preserve the distinction across OpenAI, Anthropic, worker ACK handling, metrics, documentation, and router end-to-end expectations.

Details:

Previously overload was mapped to 503 while the runtime no-worker path was untyped and could fall through to 500. Worker ACKs and the Anthropic pre-stream error path also collapsed overload and unavailable failures. This change introduces typed unavailable propagation, maps capacity rejection to 529, and records separate overload and unavailable metric labels.

Where should the reviewer start?

  • lib/llm/src/http/service/error.rs for the HTTP status contract.
  • lib/llm/src/http/service/openai.rs and lib/llm/src/http/service/anthropic.rs for protocol handling.
  • lib/runtime/src/pipeline/network/egress/push_router.rs and addressed_router.rs for typed routing errors.

Validation

  • cargo clippy -p dynamo-runtime -p dynamo-llm --no-default-features -- -D warnings
  • cargo fmt --all
  • Pre-commit hooks: isort, black, flake8, codespell, ruff, and repository hygiene checks
  • Test suites not run at the requester's direction

Related Issues

🚫 This PR is NOT linked to an issue:

  • Confirmed — no related issue

Signed-off-by: PeaBrane <yanrpei@gmail.com>
@PeaBrane
PeaBrane requested review from a team as code owners June 24, 2026 23:04
@PeaBrane
PeaBrane requested a review from a team June 24, 2026 23:04
@PeaBrane
PeaBrane temporarily deployed to external_collaborator June 24, 2026 23:04 — with GitHub Actions Inactive
@github-actions github-actions Bot added fix documentation Improvements or additions to documentation frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` labels Jun 24, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

🐛 1 issue in files not directly in the diff

🐛 Anthropic path returns 500 instead of 503/529 for Unavailable and ResourceExhausted errors (lib/llm/src/http/service/anthropic.rs:352-371)

This PR introduces ErrorType::Unavailable in push_router.rs:655-665 and properly handles it on the OpenAI path via from_anyhow (openai.rs:391-396), returning HTTP 503. However, the Anthropic endpoint at anthropic.rs:352-371 uses its own inline error handler that only checks request_was_rejected (for metrics only — no early return) and request_was_cancelled, then falls through to SanitizedError::Internal (HTTP 500) for everything else. When no workers are available, the push router now emits ErrorType::Unavailable, which the OpenAI path correctly maps to 503 but the Anthropic path misclassifies as 500 Internal Server Error. Additionally, ResourceExhausted errors on the Anthropic path only increment the rejection metric at line 353 but don't return SanitizedError::Overloaded — they also fall through to 500. The ResourceExhausted → 500 issue is pre-existing, but the Unavailable → 500 inconsistency is newly introduced because Unavailable is a new error type from this PR.

Open in Devin Review

Comment thread lib/runtime/src/pipeline/network/egress/addressed_router.rs Outdated
@github-actions

github-actions Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Updated overload handling to use HTTP 529 across runtime errors, HTTP service mappings, OpenAI responses, router tests, and docs. Added a distinct Unavailable runtime error path for no-worker cases.

Changes

HTTP 529 overload handling

Layer / File(s) Summary
Unavailable runtime path
lib/runtime/src/error.rs, lib/runtime/src/metrics/prometheus_names.rs, lib/runtime/src/pipeline/network/egress/addressed_router.rs, lib/runtime/src/pipeline/network/egress/push_router.rs, lib/llm/src/http/service/metrics.rs
ErrorType::Unavailable is added, no-worker routing returns that error, and related overload metric/comment text uses 529/429 wording.
HTTP 529 status contract
lib/llm/src/http/service/error.rs, lib/llm/src/http/service/openapi_docs.rs, lib/llm/src/http/service/busy_threshold.rs, lib/llm/src/discovery/worker_monitor.rs
SanitizedError::Overloaded now uses HTTP 529, Unavailable stays 503, and the matching OpenAPI and service doc updates are added.
OpenAI overload responses
lib/llm/src/http/service/openai.rs
OpenAI error mapping now classifies 529 overloads, separates unavailable responses, and updates the related response tests.
Published request-rejection docs
docs/fault-tolerance/request-rejection.md, docs/components/router/router-configuration.md, docs/getting-started/introduction.md, docs/getting-started/introduction.zh-CN.md
The router and load-shedding documentation now describe HTTP 529 responses, retry branching, and the updated rejection mappings.
Shared overload probe
tests/router/common.py
The shared router overload helper and KV overload scenario now assert 529 rejections and the matching metric counts.
Disagg and none-threshold router tests
tests/router/common.py
The disaggregated overload scenario, explicit none-threshold case, and busy-threshold endpoint comment now use 529-based expectations.
Router e2e mocker cases
tests/router/test_router_e2e_with_mockers.py
The mocker-based router overload cases are renamed to 529 variants and wired to the updated shared helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: separating overload handling from unavailable-worker handling.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description check ✅ Passed The PR description follows the required template and includes the overview, details, reviewer guidance, validation, and related-issues section.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/router/test_router_e2e_with_mockers.py (1)

1181-1193: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add an explicit timeout to this E2E test.

This case starts services, waits for frontend readiness, and exercises multiple HTTP calls, but it still has no @pytest.mark.timeout(...). If /busy_threshold or discovery wedges, this can hang the pytest worker indefinitely.

Suggested fix
+@pytest.mark.timeout(120)
 def test_busy_threshold_endpoint(

As per coding guidelines, "Flag any test running over 30 seconds or containing time.sleep(), polling loops, network calls, or subprocess waits without a @pytest.mark.timeout(...) marker as a required change"; as per path instructions, .ai/pytest-guidelines.md says "any polling/sleep/network/subprocess waits must have @pytest.mark.timeout(...)"; based on learnings, this repo does not configure a global pytest-timeout default.

🤖 Prompt for 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.

In `@tests/router/test_router_e2e_with_mockers.py` around lines 1181 - 1193, Add
an explicit pytest timeout marker to the busy_threshold E2E test so it cannot
hang indefinitely if discovery or /busy_threshold wedges. Update the test
function test_busy_threshold_endpoint in the router E2E suite by adding an
appropriate `@pytest.mark.timeout`(...) decorator, matching the repo’s timeout
expectations for network/waiting tests.

Sources: Coding guidelines, Path instructions, Learnings

🧹 Nitpick comments (1)
tests/router/common.py (1)

1157-1159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use lazy logging in the new overload helpers.

These new logger.* calls eagerly format strings with f-strings. Please switch them to placeholder-style logging so the helpers stay aligned with the repo’s Python logging rule.

Suggested fix
-                            logger.info(f"Request {req_id} got expected 529: {body}")
+                            logger.info(
+                                "Request %s got expected 529: %s", req_id, body
+                            )
...
-    logger.info(
-        f"Results: {num_succeeded} succeeded, {num_rejected} rejected (529), "
-        f"{num_other} other"
-    )
+    logger.info(
+        "Results: %s succeeded, %s rejected (529), %s other",
+        num_succeeded,
+        num_rejected,
+        num_other,
+    )
...
-    logger.info(
-        f"Successfully verified overload 529: {num_rejected} rejected, "
-        f"{num_succeeded} succeeded, metrics match"
-    )
+    logger.info(
+        "Successfully verified overload 529: %s rejected, %s succeeded, metrics match",
+        num_rejected,
+        num_succeeded,
+    )
...
-        logger.info(
-            f"Starting disagg KV router frontend on port {frontend_port} for overload 529 test"
-        )
+        logger.info(
+            "Starting disagg KV router frontend on port %s for overload 529 test",
+            frontend_port,
+        )

As per coding guidelines, "Use lazy formatting for log messages; pass format strings and arguments separately to logging calls instead of using f-strings or eager formatting"; as per path instructions, .ai/python-guidelines.md says to "use lazy logging (no f-strings / eager formatting inside logger.* calls)".

Also applies to: 1225-1245, 1367-1369

🤖 Prompt for 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.

In `@tests/router/common.py` around lines 1157 - 1159, Replace the eager f-string
logging in the overload helper flow with lazy placeholder-style logging so it
matches the repo’s Python logging rule. Update the relevant `logger.*` calls in
`tests/router/common.py`, including the `logger.info` in the `response.status ==
529` branch and the other helper logs around the mentioned overload sections, to
pass a format string plus arguments instead of interpolating values upfront.
Keep the same messages and context, but ensure the logging calls defer
formatting until needed.

Sources: Coding guidelines, Path instructions

🤖 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 `@lib/llm/src/http/service/metrics.rs`:
- Around line 39-45: Add a dedicated unavailable metrics classification and stop
routing all 503 responses through ErrorType::Overload. Update the HTTP error
classifier in openai.rs so request_was_unavailable() maps no-routable-worker
503s to a new ErrorType::Unavailable variant, while keeping true overload cases
on Overload. Then update test_classify_error_for_metrics_status_codes and any
related metrics assertions to expect the new unavailable value, and ensure the
metrics enum and all match sites are plumbed through consistently.

In `@lib/llm/src/http/service/openai.rs`:
- Around line 140-145: The status-to-ErrorType mapping in openai.rs is still
collapsing every 503 into ErrorType::Overload, which makes the new unavailable
path indistinguishable from overload. Update the classification logic in the
response/error mapping used by the OpenAI service so the unavailable case from
from_anyhow(...Unavailable...) and model_unavailable() is preserved as its own
category or keyed off the sanitized unavailable message, while keeping any
legacy/backend overload handling separate. Use the existing OpenAI error mapping
branch that matches StatusCode::SERVICE_UNAVAILABLE and related helpers to make
this distinction without changing other status handling.

---

Outside diff comments:
In `@tests/router/test_router_e2e_with_mockers.py`:
- Around line 1181-1193: Add an explicit pytest timeout marker to the
busy_threshold E2E test so it cannot hang indefinitely if discovery or
/busy_threshold wedges. Update the test function test_busy_threshold_endpoint in
the router E2E suite by adding an appropriate `@pytest.mark.timeout`(...)
decorator, matching the repo’s timeout expectations for network/waiting tests.

---

Nitpick comments:
In `@tests/router/common.py`:
- Around line 1157-1159: Replace the eager f-string logging in the overload
helper flow with lazy placeholder-style logging so it matches the repo’s Python
logging rule. Update the relevant `logger.*` calls in `tests/router/common.py`,
including the `logger.info` in the `response.status == 529` branch and the other
helper logs around the mentioned overload sections, to pass a format string plus
arguments instead of interpolating values upfront. Keep the same messages and
context, but ensure the logging calls defer formatting until needed.
🪄 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: Enterprise

Run ID: eab56319-d341-451d-9a2f-a1a7629937d7

📥 Commits

Reviewing files that changed from the base of the PR and between 12e2943 and 7405875.

📒 Files selected for processing (16)
  • docs/components/router/router-configuration.md
  • docs/fault-tolerance/request-rejection.md
  • docs/getting-started/introduction.md
  • docs/getting-started/introduction.zh-CN.md
  • lib/llm/src/discovery/worker_monitor.rs
  • lib/llm/src/http/service/busy_threshold.rs
  • lib/llm/src/http/service/error.rs
  • lib/llm/src/http/service/metrics.rs
  • lib/llm/src/http/service/openai.rs
  • lib/llm/src/http/service/openapi_docs.rs
  • lib/runtime/src/error.rs
  • lib/runtime/src/metrics/prometheus_names.rs
  • lib/runtime/src/pipeline/network/egress/addressed_router.rs
  • lib/runtime/src/pipeline/network/egress/push_router.rs
  • tests/router/common.py
  • tests/router/test_router_e2e_with_mockers.py

Comment thread lib/llm/src/http/service/metrics.rs
Comment thread lib/llm/src/http/service/openai.rs Outdated
Signed-off-by: PeaBrane <yanrpei@gmail.com>
@PeaBrane
PeaBrane temporarily deployed to external_collaborator June 24, 2026 23:22 — with GitHub Actions Inactive
@PeaBrane

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 97e235f:

  • Anthropic pre-stream routing errors now preserve ResourceExhausted as 529 and Unavailable as 503.
  • Worker rejection ACKs distinguish capacity overload from a closed worker-pool channel.
  • Frontend metrics now record separate overload and unavailable categories.
  • The touched router E2E logging uses lazy formatting.

The timeout finding required no change: test_busy_threshold_endpoint already has @pytest.mark.timeout(120) immediately above it.

Validation: cargo clippy for dynamo-runtime and dynamo-llm with --no-default-features and warnings denied, cargo fmt, and all pre-commit hooks passed. Test suites were not run.

@PeaBrane
PeaBrane merged commit 73a71bf into main Jun 25, 2026
101 checks passed
@PeaBrane
PeaBrane deleted the codex/overload-529 branch June 25, 2026 01:10

@dynamo-review-agent dynamo-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lib/llm/src/grpc/service/openai.rs — The new router ErrorType::Unavailable falls through to Status::internal in the gRPC completions generate path, so no-worker requests return gRPC code 13 instead of retryable code 14. Fix: handle request_was_unavailable before the internal fallback and return Status::unavailable.

🤖 AI Fix

In lib/llm/src/grpc/service/openai.rs, inside completion_response_stream's engine.generate(request).await.map_err closure, add if crate::http::service::metrics::request_was_unavailable(e.as_ref()) { return Status::unavailable(e.to_string()); } immediately after the request_was_rejected branch and before Status::internal(...).

lib/llm/src/grpc/service/tensor.rs — The new router ErrorType::Unavailable falls through to Status::internal in the gRPC tensor generate path, so no-worker requests return gRPC code 13 instead of retryable code 14. Fix: handle request_was_unavailable before the internal fallback and return Status::unavailable.

🤖 AI Fix

In lib/llm/src/grpc/service/tensor.rs, inside tensor_response_stream's engine.generate(request).await.map_err closure, add if crate::http::service::metrics::request_was_unavailable(e.as_ref()) { return Status::unavailable(e.to_string()); } immediately after the request_was_rejected branch and before Status::internal(...).

let error_type = if res_bytes.starts_with(OVERLOAD_PREFIX) {
ErrorType::ResourceExhausted
} else if res_bytes.starts_with(UNAVAILABLE_PREFIX) {
ErrorType::Unavailable

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mapping the closed worker-pool ACK to ErrorType::Unavailable means PushRouter::wrap_with_fault_detection no longer marks that selected worker overloaded or down, so the router can keep selecting a worker that has already reported its ingress channel closed. Fix: treat ErrorType::Unavailable from a selected worker as an inhibited/faulted instance and remove it from routing candidates.

🤖 AI Fix

In lib/runtime/src/pipeline/network/egress/push_router.rs, update is_inhibited to include ErrorType::Unavailable in the INHIBITED list, or add an equivalent branch in PushRouter::wrap_with_fault_detection before the ResourceExhausted branch that calls self.client.report_instance_down(instance_id) when match_error_chain(err.as_ref(), &[ErrorType::Unavailable], &[]) is true; add a unit test covering a "Server unavailable: worker pool channel closed" ACK causing the selected instance to be reported down.

@PeaBrane PeaBrane changed the title fix(router): distinguish overload from unavailable fix(router): distinguish overload from unavailable [DYN-3174] Jun 25, 2026
@PeaBrane PeaBrane changed the title fix(router): distinguish overload from unavailable [DYN-3174] fix(router): distinguish overload from unavailable [DYN-3279] Jun 25, 2026

This branch was previously deployed

2 inactive deployments
GITLAB — 97e235f0 Deployed Jun 24, 2026 by copy-pr-bot[bot] via Trigger CI Pipeline #31961
external_collaborator — 97e235f0 Deployed Jun 24, 2026 by PeaBrane via ok-to-test #224
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation fix frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants