Skip to content

egress: return a retryable 503 instead of a 504 when a credential provider is slow - #2787

Merged
Zoe Zhao (zoez7) merged 1 commit into
agent-substrate:mainfrom
BenTheElder:egress-credential-fetch-timeout
Oct 17, 2026
Merged

Zoe Zhao (zoez7) merged 1 commit into
agent-substrate:mainfrom
BenTheElder:egress-credential-fetch-timeout

Conversation

@BenTheElder

Copy link
Copy Markdown
Collaborator

fixes a flake in TestActorEgressCredentialInjection/https on the envoy lane:
https://github.com/agent-substrate/substrate/actions/runs/38982841766/job/117247440573

The test restarts k8s-credential-provider and then makes the first injected request. The gateway's FetchSecret to the new pod had no deadline, so a slow first call ran past the ext_proc message_timeout (5s) and envoy answered 504 with an empty body (failed at 5.18s). The test already retries 503 while the gateway reconnects to the provider, but not 504.

The policy fetch already handles this by staying under the message timeout (4s):

// policyFetchTimeout caps one GetActorEgressPolicy call. It sits under the
// ext_proc message_timeout (5s in both manifests) so a slow control plane is a
// 503 rather than an Envoy timeout.
const policyFetchTimeout = 4 * time.Second

This puts the same 4s cap on resolving a request's credentials (provider fetch and actor JWT mint), so a hung provider is the documented retryable 503.

agentgateway has no outer message timeout, so it waits on a slow provider instead of returning 504 and doesn't hit this, otherwise I would've proposed alignment.

AI-assisted.

a hung provider call ran into the ext_proc message_timeout and came back as a 504, same cap as the policy fetch so it's a retryable 503
@BenTheElder Benjamin Elder (BenTheElder) added kind/bug Something isn't working / bugfixes area/network labels Oct 17, 2026
@zoez7
Zoe Zhao (zoez7) added this pull request to the merge queue Oct 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Makes sense, and wrapping the whole injections loop rather than each FetchSecret looks deliberately right - the ext_proc budget is per message, not per call, so a shared budget is the thing that actually has to fit.

One thing I wondered about while reading. injectionTimeout is 4s against defaultExtProcMessageTimeout of 5s, which matches policyFetchTimeout. But the message timeout isn't always the default: dataplane.go:77 calls

xdsSrv.SetExtProcMessageTimeout(parkCfg.Budget + 5*time.Second)

when parking is enabled, so the effective timeout grows with the park budget while these two constants stay at 4s. In that configuration a credential fetch that takes, say, 6s gets cut off at 4s and answered 503, where Envoy would have waited. Retryable rather than broken, so not a big deal - but it's the mirror of the flake you're fixing.

The repo already has the derive-rather-than-hardcode shape for this: drainTimeoutMargin exists specifically to mirror "the +5s Envoy ext_proc MessageTimeout margin". Would deriving both fetch timeouts from the effective message timeout be worth a follow-up? policyFetchTimeout has the same coupling, so this PR isn't making anything worse either way.

@BenTheElder

Copy link
Copy Markdown
Collaborator Author

Abhishek Sharma (@abhisheksharma2411) Thanks for checking, but I don't think this applies to egress. SetExtProcMessageTimeout is on the xDS server, which only configures the ingress envoy. An egress-only atenet doesn't run it:

// The ingress control plane — the xDS server — configures the *ingress*
// dataplane. The egress gateway is statically configured, so an egress-only
// instance does not run it.
if s.cfg.Mode.ServesIngress() {
if err := s.startDataplane(ctx, g, parkCfg, sampling.RootSamplingPercent()); err != nil {
return err
}

The egress envoy has message_timeout: 5s in its static config, and parking doesn't apply there:

So the real coupling is the yaml vs the two go constants. We can't really derive one from the other.

Merged via the queue into agent-substrate:main with commit 115d1a0 Oct 17, 2026
15 checks passed
@BenTheElder
Benjamin Elder (BenTheElder) deleted the egress-credential-fetch-timeout branch October 17, 2026 01:26
@abhisheksharma2411

Copy link
Copy Markdown

You're right, and I should have checked that rather than assuming the xDS path applied here. router.go even spells it out:

// The ingress control plane — the xDS server — configures the *ingress*
// dataplane. The egress gateway is statically configured, so an egress-only
// instance does not run it.
if s.cfg.Mode.ServesIngress() {

So SetExtProcMessageTimeout never touches egress, and the credential injection path is against a fixed 5s from the static config. The parking case I described can't happen. Sorry for the noise.

Agreed the real coupling is the yaml against the two Go constants, and that isn't something you can derive. Nothing blocking from me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/network kind/bug Something isn't working / bugfixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants