Skip to content

feat(core): add execute_with_resilience as primary resilience API - #811

Closed
CatherineSue wants to merge 2 commits into
mainfrom
feat/execute-with-resilience
Closed

CatherineSue wants to merge 2 commits into
mainfrom
feat/execute-with-resilience

Conversation

@CatherineSue

@CatherineSue CatherineSue commented Mar 18, 2026 •

Copy link
Copy Markdown
Member

Description

Part of the per-worker resilience refactor series: #799 → #803 → this PR → router migration → cleanup.

Problem

Routers currently call RetryExecutor::execute_response_with_retry() directly, each duplicating the same retry/CB boilerplate — selecting retry config, wiring should_retry, calling record_outcome, etc. There's no single entry point that uses the worker's own resolved resilience config.

Solution

Add execute_with_resilience() as the primary resilience API. It encapsulates the full retry + circuit breaker flow using the worker's own ResolvedResilience config:

  1. Checks circuit breaker before first attempt (returns 503 if open)
  2. If retries disabled → single attempt with CB outcome recording
  3. If retries enabled → delegates to RetryExecutor with the worker's retry config and is_retryable() predicate

Routers will migrate to this in subsequent PRs.

Changes

  • Add execute_with_resilience() function in model_gateway/src/core/resilience.rs
  • Export from model_gateway/src/core/mod.rs
  • 5 new async tests: success path, circuit breaker open, retries disabled, retry on retryable status, CB disabled bypass

Test Plan

  • cargo test -p smg --lib core::resilience — all 12 tests pass (7 existing + 5 new)
  • cargo test -p smg --lib — all 440 tests pass, 0 failures
  • Pre-commit hooks pass (rustfmt, clippy, codespell, DCO)
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

Summary by CodeRabbit

  • New Features
    • Enhanced resilient request execution with retry and circuit-breaker protection to improve reliability and prevent cascading failures.
    • Improved observability: backoff logging, metrics hooks, and a single-try mode when retries are disabled.
  • Tests
    • Added coverage for success, open-circuit, retry behavior, and retry-disabled scenarios.

Summary by CodeRabbit

Add execute_with_resilience() function that encapsulates retry loop,
circuit breaker checks, and outcome recording using the worker's own
resolved config. This is the single entry point routers will use
instead of calling RetryExecutor directly.

- Checks circuit breaker before first attempt (when CB enabled)
- Skips retry loop when retries are disabled (single attempt)
- Uses worker's is_retryable() for retry decisions
- Records backoff metrics via Metrics::record_worker_retry_backoff
- 5 new async tests covering success, CB open, retries disabled,
  retry on retryable status, and CB disabled bypass

Signed-off-by: Chang Su <chang.s.su@oracle.com>
@CatherineSue
CatherineSue requested a review from slin1237 as a code owner March 18, 2026 22:26
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a unified resilience API, execute_with_resilience(), to streamline how retry and circuit breaker patterns are applied to worker requests. This change aims to reduce redundant code across routers by providing a single entry point that automatically applies the worker's resolved resilience configuration, thereby improving consistency and maintainability of the system's fault tolerance mechanisms.

Highlights

  • New Resilience API: Introduced execute_with_resilience() as the primary API for handling request resilience, centralizing retry and circuit breaker logic.
  • Encapsulated Logic: The new API encapsulates the full retry and circuit breaker flow, utilizing the worker's ResolvedResilience configuration.
  • Circuit Breaker Integration: It checks the circuit breaker status before the initial attempt, returning a 503 if the circuit is open.
  • Conditional Retries: The function handles requests with retries disabled (single attempt) or enabled, delegating to RetryExecutor with worker-specific retry configuration and is_retryable() predicate.
  • New Tests: Added five new async tests covering success, open circuit breaker, disabled retries, retries on retryable status, and disabled circuit breaker scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented Mar 18, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds and publicly re-exports a new async function execute_with_resilience that orchestrates per-worker request execution with circuit-breaker checks, retry delegation, backoff logging, and metrics hooks; includes tests and a single-try helper path.

Changes

Cohort / File(s) Summary
Core Module Re-exports
model_gateway/src/core/mod.rs
Re-exported execute_with_resilience from the resilience module alongside resolve_resilience, ResolvedResilience, and DEFAULT_RETRYABLE_STATUS_CODES.
Resilience Implementation & Tests
model_gateway/src/core/resilience.rs
Added pub async fn execute_with_resilience<F, Fut>(worker: &(dyn Worker + '_), operation: F) -> axum::response::Response with circuit-breaker pre-checks, single-try path when retries disabled, delegation to RetryExecutor when enabled, backoff logging, metrics hooks, and comprehensive unit tests covering success, open circuit, retries-disabled, retry flows, and CB-disabled scenarios.

Sequence Diagram(s)

sequenceDiagram
    actor Caller
    participant EWR as execute_with_resilience
    participant CB as CircuitBreaker
    participant Worker as Worker
    participant Retry as RetryExecutor
    participant Metrics as Metrics/Logger

    Caller->>EWR: execute_with_resilience(worker, operation)
    EWR->>CB: check state
    alt circuit open
        CB-->>EWR: open
        EWR->>Metrics: record rejection
        EWR-->>Caller: 503 response
    else circuit closed/half-open
        alt retries disabled
            EWR->>Worker: execute_single(operation)
            Worker-->>EWR: response
            EWR->>CB: record outcome
            EWR-->>Caller: response
        else retries enabled
            EWR->>Retry: start retry loop
            loop retry attempts
                Retry->>Worker: execute(operation)
                Worker-->>Retry: response
                Retry->>Metrics: log attempt/backoff
                alt retryable
                    Retry->>Retry: backoff & retry
                else final
                    Retry-->>EWR: final response
                end
            end
            EWR->>CB: record final outcome
            EWR-->>Caller: response
        end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Reviewers

  • slin1237
  • whybeyoung

Poem

🐰 I hopped through code with careful paws,
Circuit-flowers guarding the cause.
Retries I counted, metrics I sing,
One brave request took flight on spring —
Hooray for resilient things! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding execute_with_resilience as the primary resilience API. It accurately reflects the core functionality being introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ 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 feat/execute-with-resilience
📝 Coding Plan
  • Generate coding plan for human review comments

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

@github-actions github-actions Bot added the model-gateway Model gateway crate changes label Mar 18, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18b23114f7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/core/resilience.rs
Comment thread model_gateway/src/core/resilience.rs

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new execute_with_resilience function to centralize retry and circuit breaker logic. While this is a valuable addition, the current implementation has a critical flaw: it fails to record circuit breaker outcomes when retries are enabled, which undermines the circuit breaker's effectiveness. I've provided a detailed comment with a suggested fix that addresses this by adjusting the function signature to correctly handle lifetimes and ensure outcomes are recorded on every attempt. I've also suggested a minor cleanup to remove a now-redundant helper function.

Comment thread model_gateway/src/core/resilience.rs Outdated
Comment thread model_gateway/src/core/resilience.rs Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/core/resilience.rs`:
- Around line 141-145: The response currently leaks the internal worker_url by
returning format!("Circuit breaker open for worker {worker_url}") as the 503
body; instead, return a generic message (e.g., "Service unavailable") for the
StatusCode::SERVICE_UNAVAILABLE response and move the worker_url detail into a
debug! or error! log call so the internal address is only logged (keep the
existing debug! usage). Update the code that constructs the response (the block
invoking StatusCode::SERVICE_UNAVAILABLE and format! for the circuit breaker
case in resilience.rs) to use a generic body while preserving the worker_url in
logging.
- Around line 159-176: The retry-enabled branch currently awaits
RetryExecutor::execute_response_with_retry(...) and returns its result without
invoking the circuit-breaker outcome recorder; capture the returned result from
RetryExecutor::execute_response_with_retry into a variable (e.g., let result =
... .await), call the same record_outcome(...) function used in the non-retry
branch with that result (matching the breaker and outcome signature used
elsewhere), and then return the captured result; ensure you still perform
Metrics::record_worker_retry_backoff and the debug logs as before and that you
use the same operation/resilience/retry and worker.is_retryable arguments to
preserve behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba851e8b-4dbe-4a29-95d9-84b70182ae38

📥 Commits

Reviewing files that changed from the base of the PR and between 924d10a and 18b2311.

📒 Files selected for processing (2)
  • model_gateway/src/core/mod.rs
  • model_gateway/src/core/resilience.rs

Comment thread model_gateway/src/core/resilience.rs
Comment thread model_gateway/src/core/resilience.rs Outdated
- Record circuit breaker outcome after retry loop completes, not just
  in the no-retry path — fixes bug where CB never learned from retried
  requests
- Replace internal worker URL in 503 body with generic message to
  avoid leaking backend addresses to clients
- Inline execute_single helper (was just operation(0).await)

Signed-off-by: Chang Su <chang.s.su@oracle.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c320e017e0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +124 to +126
pub async fn execute_with_resilience<F, Fut>(
worker: &(dyn Worker + '_),
mut operation: F,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve per-attempt worker selection for retries

I checked model_gateway/src/routers/http/router.rs:216-221 and :281-289: each retry currently re-enters select_worker_for_model, and that selector filters w.is_available() at :152-155. This helper bakes a single Worker into the retry loop, so any router migration will retry the same backend instead of failing over to another healthy worker. In multi-worker pools, a transient 429/503 from one instance will become a user-visible failure even when capacity is still available elsewhere.

Useful? React with 👍 / 👎.

Comment on lines +159 to +162
let response = RetryExecutor::execute_response_with_retry(
&resilience.retry,
operation,
|res, _attempt| worker.is_retryable(res),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-check circuit-breaker state before retrying

This path calls can_execute() only once before handing control to RetryExecutor. If another request opens the same worker's breaker while this call is sleeping in backoff, the next attempt still invokes operation on that worker because there is no per-attempt gate here. The current HTTP flow avoids that by selecting an available worker on every attempt (model_gateway/src/routers/http/router.rs:152-155, :216-221, :281-289), so migrating to this helper would let concurrent failure bursts keep hammering a worker that the breaker has already quarantined.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/core/resilience.rs`:
- Around line 242-274: Add an assertion after the existing response checks in
the test that verifies the circuit-breaker state/outcome was updated by the
retry-enabled path: after calling execute_with_resilience(&worker, ...) and
asserting StatusCode::OK and call_count, query the worker's resilience/breaker
(the breaker instance configured via ResolvedResilience/RetryConfig on
BasicWorkerBuilder::resilience) using the actual breaker API in the codebase
(e.g., the breaker state or last outcome accessor) and assert it reflects a
successful outcome (closed/success or equivalent enum variant). Ensure you
reference the same worker variable and use the breaker state accessor
implemented on the worker/resilience types so the test fails if breaker state is
not updated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 686882ef-6f61-427c-8b6f-6123897f57f6

📥 Commits

Reviewing files that changed from the base of the PR and between 18b2311 and c320e01.

📒 Files selected for processing (1)
  • model_gateway/src/core/resilience.rs

Comment on lines +242 to +274
#[tokio::test]
async fn test_execute_with_resilience_retries_on_retryable_status() {
let resolved = ResolvedResilience {
retry: RetryConfig {
max_retries: 3,
initial_backoff_ms: 1,
max_backoff_ms: 2,
backoff_multiplier: 1.0,
jitter_factor: 0.0,
},
..Default::default()
};
let worker = BasicWorkerBuilder::new("http://test:8080")
.resilience(resolved)
.build();

let call_count = Arc::new(AtomicU32::new(0));
let cc = call_count.clone();
let response = execute_with_resilience(&worker, move |_attempt| {
let count = cc.fetch_add(1, Ordering::Relaxed);
async move {
if count < 2 {
(StatusCode::SERVICE_UNAVAILABLE, "fail").into_response()
} else {
(StatusCode::OK, "ok").into_response()
}
}
})
.await;

assert_eq!(response.status(), StatusCode::OK);
assert_eq!(call_count.load(Ordering::Relaxed), 3);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add a regression test that asserts breaker state updates in the retry-enabled path.

Current tests validate status and retry counts, but there is no explicit assertion that breaker outcome/state changes after the retry loop. A focused assertion here would lock in this fix against regression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/core/resilience.rs` around lines 242 - 274, Add an
assertion after the existing response checks in the test that verifies the
circuit-breaker state/outcome was updated by the retry-enabled path: after
calling execute_with_resilience(&worker, ...) and asserting StatusCode::OK and
call_count, query the worker's resilience/breaker (the breaker instance
configured via ResolvedResilience/RetryConfig on BasicWorkerBuilder::resilience)
using the actual breaker API in the codebase (e.g., the breaker state or last
outcome accessor) and assert it reflects a successful outcome (closed/success or
equivalent enum variant). Ensure you reference the same worker variable and use
the breaker state accessor implemented on the worker/resilience types so the
test fails if breaker state is not updated.

@CatherineSue

Copy link
Copy Markdown
Member Author

Closing — the per-worker retry approach is wrong. Retries should re-select workers (like Envoy), not retry the same backend. The existing router retry loops already do this correctly. Per-worker improvements (http_client, is_retryable, resilience config) will be adopted directly within the existing router retry loops in subsequent PRs.

@zhyncs
zhyncs deleted the feat/execute-with-resilience branch March 19, 2026 06:13
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant