feat(tonic-xds): drive gRPC retry config from RDS RouteAction.retry_policy - #2786
Merged
Merged
Conversation
LYZJU2019
force-pushed
the
lyzju2019/xds-transport-channel
branch
from
July 31, 2026 19:13
99cfc82 to
bf14624
Compare
LYZJU2019
force-pushed
the
lyzju2019/xds-transport-channel
branch
from
August 6, 2026 22:55
bf14624 to
48d06bb
Compare
LYZJU2019
force-pushed
the
lyzju2019/xds-transport-channel
branch
4 times, most recently
from
August 12, 2026 18:39
bd95f5c to
f8015d3
Compare
Contributor
|
A few issues with the current code that require fundamental design change:
Let me know if you have any questions re: the design proposal. |
LYZJU2019
force-pushed
the
lyzju2019/xds-transport-channel
branch
from
August 12, 2026 22:29
f8015d3 to
c3261a3
Compare
YutaoMa
reviewed
Aug 12, 2026
…olicy Wire the gRPC channel's retry configuration to the control plane so retry behavior tracks RDS (RouteConfiguration) updates without rebuilding the channel. The retry settings come from the standard Envoy `RouteAction.retry_policy` (gRFC A44), so OSS parses them natively — no caller-supplied extractor is needed. Retry is per route: each request retries according to the exact route it matched. - Parse and validate the retry policy when the RDS resource is validated — once, not per request. `validate_route` maps the Envoy `retry_on` conditions to gRPC status codes, applies defaults for unset fields, and stores the result as an immutable, `Arc`-shared `GrpcRetrySharedConfig` on the matched route (`RouteConfig.retry_config`). A route uses its own `RouteAction.retry_policy` when set, otherwise it inherits the enclosing `VirtualHost.retry_policy` (gRFC A44: a route-level policy completely overrides the virtual host's — values are not merged). `None` when neither specifies retry; routes inheriting the vhost policy share one `Arc`. `RouteRetryConfig` remains a transport-neutral carrier for the raw Envoy values during parsing. - Select the config per route via the routing decision. The routing layer stamps the matched route's shared retry config into the request's `RouteDecision`, taken from the same config snapshot it routed with. Because routing and retry read one snapshot, they always act on the same RDS version (no cross-layer skew), and retry runs inside routing so the decision is fixed across a request's retry attempts. - Separate the shared, immutable retry config (attempt cap, backoff, retryable code set) from the per-request retry state (backoff cursor, attempt count). `RetrySharedConfig<C>` holds the config behind an `Arc`; `RetryPolicy<C>` holds a pointer to it plus the per-request state. Instantiating a policy for a request (`RetryPolicy::from_shared`) is an `Arc` pointer clone plus a zero-field state init — the request hot path does no parsing or allocation. Requests with no `RouteDecision` (non-xDS callers) or whose route carries no retry policy use the layer's fallback config. - Map Envoy `retry_on` conditions to gRPC status codes (gRFC A44) with `grpc_retry_on_codes`; non-gRPC tokens are ignored (connection-level retries are handled separately). Envoy `numRetries` maps directly to `RetryConfig.num_retries` (retries, not attempts). The retry engine (`RetryPolicy`, `RetrySharedConfig`, and the `RetryClassifier` seam) stays transport-agnostic; only the retry layer is gRPC-specific, because it reads the concrete `RouteDecision` extension. Deriving and validating the gRPC retry config once at RDS-validation time — rather than in the transport- neutral resource type — is what keeps the request path allocation-free.
LYZJU2019
force-pushed
the
lyzju2019/xds-transport-channel
branch
from
August 12, 2026 23:17
c3261a3 to
65b6b20
Compare
YutaoMa
reviewed
Aug 12, 2026
YutaoMa
reviewed
Aug 12, 2026
YutaoMa
reviewed
Aug 12, 2026
YutaoMa
reviewed
Aug 12, 2026
YutaoMa
reviewed
Aug 12, 2026
Compile the gRPC retry config once per RDS update in a new RoutingSnapshot (client layer), keeping the xDS resource types free of gRPC business logic; the resource layer now carries only the validated RouteRetryConfig. Routing and retry read one bundled snapshot, so the request hot path stays a map lookup plus an Arc clone. - Reject out-of-range google.protobuf.Duration values and guard backoff max-interval math with checked_mul (gRFC A44). - Skip retry parsing for routes that are dropped during validation. - Treat an empty parsed retry_on set as "no policy" (return None) so it does not mask connection-level retries. - RetryLayer::new takes the shared config Arc directly.
State the compile-once, single-Arc consistency, and hot-path rationale once on RoutingSnapshot, and keep the other retry-config sites terse. No code change.
YutaoMa
reviewed
Aug 13, 2026
YutaoMa
reviewed
Aug 13, 2026
Address review: build_grpc_channel_from_parts now takes Arc<GrpcRetrySharedConfig> directly instead of a GrpcRetryPolicy it converts, and the now test-only retry-config imports live in the tests module.
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.
Summary
Drives the gRPC channel's retry configuration from the control plane so retry behavior tracks RDS (
RouteConfiguration) updates without rebuilding the channel.The retry settings come from the standard Envoy
RouteAction.retry_policy(gRFC A44), sotonic-xdsparses them natively — no caller-supplied extractor closure is required. This is additive and non-breaking; there is no public API change.Built on top of #2794. Retry is per route: each request retries according to the exact route it matched. The Envoy retry policy is validated when the
RouteConfigurationis validated; the gRPC retry config is compiled once per RDS update and shared behind anArc, so the request hot path just looks it up and clones that pointer, doing no parsing or allocation.What changed
RouteRetryConfig::from_protovalidates the EnvoyRetryPolicy(gRFC A44 — NACK onnumRetries == 0, non-positivebaseInterval/maxInterval, or out-of-rangegoogle.protobuf.Durationvalues) and stores the validated, transport-neutral values on the matched route asRouteConfig.retry_config: Option<Arc<RouteRetryConfig>>. A route uses its ownRouteAction.retry_policywhen set, otherwise it inherits the enclosingVirtualHost.retry_policy(gRFC A44: a route-level policy completely overrides the virtual host's — values are not merged);Nonewhen neither specifies retry, and routes inheriting the vhost policy share oneArc. The xDS resource types hold no gRPC-specific types.RoutingSnapshot(client layer) bundles the validatedRouteConfigResourcewith a map ofGrpcRetrySharedConfigs compiled from it once per RDS update (theXdsRouterwatch task), keyed by route-config identity. The routing layer resolves the matched route, looks up its compiled retry config in that same snapshot, and stamps it into the request'sRouteDecision. Because routing and retry read one snapshot, they always act on the same RDS version (no cross-layer skew), and retry runs inside routing so the decision is fixed across a request's retry attempts. Bundling both in oneArckeeps the request hot path to a map lookup plus a pointer clone.RetrySharedConfig<C>holds the immutable config (attempt cap, backoff, retryable code set) behind anArc;RetryPolicy<C>holds a pointer to it plus the per-request retry state (backoff cursor, attempt count). Instantiating a policy for a request (RetryPolicy::from_shared) is anArcpointer clone plus a zero-field state init — the request hot path does no parsing or allocation. Requests with noRouteDecision(non-xDS callers), whose route carries no retry policy, or whoseretry_onmaps to no gRPC status code use the layer's fallback config.retry_onto gRPC status codes.grpc_retry_on_codesmaps Envoyretry_onconditions to gRPC status codes (gRFC A44); non-gRPC tokens are ignored (connection-level retries are handled separately). When the mapped set is empty, the route installs no status-code policy — it falls back to the layer default rather than masking connection retries. EnvoynumRetriesmaps directly toRetryConfig.num_retries(retries, not attempts); unset Envoy fields fall back toRetryConfigdefaults.The retry engine (
RetryPolicy,RetrySharedConfig, and theRetryClassifierseam) stays transport-agnostic; only the retry layer is gRPC-specific, because it reads the concreteRouteDecisionextension. The xDS resource types stay free of gRPC business logic: the gRPC retry config is compiled in the client/routing layer, once per RDS update, which is what keeps the request path allocation-free.Example RDS consumed
{ "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", "name": "AccessControlApi", "virtualHosts": [{ "name": "AccessControlApi", "domains": ["*"], "routes": [{ "match": { "prefix": "" }, "route": { "cluster": "AgentLifecycleGrpc|0", "timeout": "60s", "retryPolicy": { "retryOn": "unavailable", "numRetries": 2, "retryBackOff": { "baseInterval": "0.100s", "maxInterval": "1s" } } } }] }] }The
retryPolicymay equivalently be set at thevirtualHosts[*]level, in which case every route in the virtual host that doesn't set its own inherits it.Testing
cargo +1.97.0with--features tls-ring,testutil:fmt --check,check(lib + tests),test(414 unit + 4 doc),docwithRUSTDOCFLAGS="-D warnings"(default +tls-ring),clippy(no new warnings)cargo +nightly-2025-10-18 check-external-types --all-features(no allowlist change — no public API change)New unit tests cover the mapping, the split config/state model, the RDS-validation rules, and the virtual-host/route precedence:
from_route_retryfield and default mapping ontoGrpcRetrySharedConfig, and itsNoneresult when the mapped code set is empty;from_sharedinstantiating zeroed per-request state while sharing the config by pointer; per-request state independence across cloned policies; RDS validation NACKing invalid retry policies (zero retries, non-positive or out-of-range intervals); and a route overriding the virtual-host policy while sibling routes with no policy share the inherited configArc(plus the no-policy-anywhere case yieldingNone).test_retry_once_on_unavailableexercises a real retry withretry_on = "unavailable".