GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [pipeline-manager] Keep cached JWKS keys when a refresh fails by blp · Pull Request #7865 · feldera/feldera

Conversation

@blp

@blp blp commented Nov 5, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Linked Issue

Related to #7048. This is the likely cause, but it is not confirmed from the instance logs.

In run 41968241434, the client sent a token that had not expired, and its retry sent the same token. Both got 401. So the instance refused a valid token.

Cached JWKS keys expired 120 s after a fetch. A failed refresh starts the 30 s federated cooldown, and during it every cache miss is refused. So one failed fetch of the GitHub JWKS refused every CI token for 30 s:

Known kid, keys older than 120 s Before After
Refresh succeeds key key
Refresh fails, or issuer cooling down 401 cached key, plus a warning in the log
Refresh succeeds, but the kid was removed 401 401
Keys older than 1 h, refreshes failing n/a 401

Rationale

  • A failed fetch says the issuer is unreachable, not that our keys are wrong. Issuers publish a new key before they sign with it, and they keep the old key published for a time after. So keys from a recent successful fetch are almost always still correct. Refusing every token because of a network error is the worse choice.
  • The cooldown stays as it was. It limits an outage to one fetch per cooldown. A shorter cooldown would cause more fetches during an outage and would still refuse tokens for the shorter time.
  • The fix is on the server, not in the CI client. A longer client retry would hide the problem in CI only. Other federated clients would still get 30 s of 401s.
  • Risk: a key that the issuer removed, for example after a leak. We see a removal only on a successful refresh. A successful refresh replaces all keys of the issuer, so a removed key is refused at once. While the issuer stays unreachable, the cached keys stay in use for at most 1 h. That is long enough for a short outage, and it bounds how long a removed key is still accepted.

This touches auth.rs together with #7862. The two changes are independent, but one of them will need a rebase.

Describe Manual Test Plan

New unit tests. Each fails when its part of the fix is reverted:

Test Reverted change that makes it fail
a_failed_refresh_keeps_the_cached_keys usable() limited to the fresh age
a_refresh_drops_keys_the_issuer_removed refresh merges keys instead of replacing them
concurrent_stale_hits_survive_a_failed_refresh (suggested in review) the cooldown branch refuses stale keys

The last test covers the CI case: requests that wait on the gate while a refresh fails. It also checks both edges of the 1 h limit. All 28 auth:: tests pass, and clippy is clean.

Checklist

  • Unit tests added/updated
  • Integration tests added/updated
  • Documentation updated
  • Changelog updated

Breaking Changes?

  • OpenAPI / REST HTTP API / feldera-types / manager
  • Feldera SQL (Syntax, Semantics)
  • feldera-sqllib
  • Python SDK
  • fda (CLI arguments)
  • Adapters (including configuration)
  • Storage Format / Checkpoints
  • Others (specify)

Describe Incompatible Changes

None.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp blp added bug Something isn't working Pipeline manager Pipeline manager (API, API server, runner, compiler server) rust Pull requests that update Rust code authentication Everything related to OIDC, AWS Cognito, auth0, tenants, issuers etc. labels Nov 5, 2026
@blp blp self-assigned this Nov 5, 2026
@blp blp mentioned this pull request Nov 5, 2026
arm64 Runtime Integration Tests: OIDC token starts getting 401 'credential rejected' ~40min into the run #7048
Closed
@mythical-fred-oss

mythical-fred-oss Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review: LGTM.

What I ran:

Check Result
cargo test --locked -p pipeline-manager --lib auth:: 27/27 pass
Extra QA test (below): concurrent stale hits during a failed refresh, plus both edges of the stale limit pass

The gates are met: there are unit tests for both halves of the fix, and they use paused tokio time, so they cannot flake. The changelog is updated, no dependencies change (cached is still used in oidc/userinfo.rs), and there is no unsafe or API change. The logic checks out:

  • A successful refresh replaces the issuer's keys, so a removed kid returns 401 again.
  • A stale key is served only from the last successful fetch.
  • Retries during an outage stay limited to one fetch per cooldown.

Missing (low-medium), test coverage: no test covers requests that queue on the gate while a refresh fails. Those requests reach the cooling_down → usable branch, which is the scenario CI actually hits. The tests also don't check the exact edges of JWK_STALE_LIMIT. Suggested test:

+    /// Concurrent stale hits during an outage share one failed fetch, and the
+    /// request queued on the gate still gets the cached key.
+    #[tokio::test(start_paused = true)]
+    async fn concurrent_stale_hits_survive_a_failed_refresh() {
+        let cache = Arc::new(Mutex::new(super::IssuerJwkCache::new()));
+        let fetches = Arc::new(AtomicUsize::new(0));
+        assert!(resolve_counting(&cache, &fetches, "real", true).await.is_ok());
+        tokio::time::advance(super::JWK_FRESH_FOR).await;
+        let (first, second) = tokio::join!(
+            resolve_counting(&cache, &fetches, "real", false),
+            resolve_counting(&cache, &fetches, "real", false),
+        );
+        assert!(first.is_ok() && second.is_ok());
+        assert_eq!(2, fetches.load(Ordering::SeqCst));
+        // Just inside the stale limit, after the cooldown: still usable.
+        tokio::time::advance(super::JWK_STALE_LIMIT - super::JWK_FRESH_FOR - Duration::from_millis(1)).await;
+        assert!(resolve_counting(&cache, &fetches, "real", false).await.is_ok());
+        tokio::time::advance(Duration::from_millis(1) + COOLDOWN).await;
+        assert!(resolve_counting(&cache, &fetches, "real", false).await.is_err());
+    }
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx

swanandx commented Nov 5, 2026

Copy link
Copy Markdown
Member

happy to rebase #7862 on top of this / when this merges

blp reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp force-pushed the jwks-serve-stale branch from 72d4e56 to cd5d708 Compare November 5, 2026 21:40
@blp
blp requested a review from swanandx November 5, 2026 22:25
@blp
blp force-pushed the jwks-serve-stale branch from cd5d708 to 04de839 Compare November 5, 2026 22:30
@blp

blp commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Added concurrent_stale_hits_survive_a_failed_refresh, based on your suggestion. One change: the far edge is now checked exactly at JWK_STALE_LIMIT, inside the cooldown of the fetch just before, and the test asserts that no extra fetch happens. With the cooldown branch reverted to refuse stale keys, the test fails on the request that waited on the gate.

-- Claude Code, replying at @blp's request.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

swanandx
swanandx approved these changes Nov 6, 2026
View reviewed changes
@blp @swanandx
[pipeline-manager] Keep cached JWKS keys when a refresh fails.
3003873
Cached signing keys expired 120 s after a fetch.  The next request then
refreshed them, and every refresh, success or failure, starts a cooldown
that rejects cache misses without a fetch.  So one failed JWKS fetch
refused every token of the issuer with a 401 for the whole cooldown,
30 s for federated tokens.  CI authenticates to its instances with
federated GitHub tokens, and its retry of a 401 comes too soon to
outlast that.

A failed fetch tells us that the issuer is unreachable, not that the
keys we have are wrong.  Issuers publish a new key before they sign with
it and keep the old one published for a time after, so keys from a recent
successful fetch are almost always still correct.  Refusing every token
because of a network error is the worse choice.

So keys older than 120 s are still refreshed on use, but if the refresh
fails, or the issuer is cooling down, the keys from the last successful
refresh stay in use.  The cooldown stays as it was, because it is what
keeps an outage to one fetch per cooldown.  Fixing only the CI client
would leave other federated clients with the same 30 s of 401s.

The risk is a key that the issuer removed, for example after a leak.
We see the removal only on a successful refresh.  A successful refresh
replaces all keys of the issuer, so a removed key is refused at once.
While the issuer stays unreachable, the cached keys stay in use for at
most 1 hour, long enough for a short outage and short enough to bound
the time a removed key is still accepted.

Related to #7048.

Signed-off-by: Ben Pfaff <[email protected]>
@swanandx
swanandx force-pushed the jwks-serve-stale branch from 04de839 to 3003873 Compare November 6, 2026 07:01
@swanandx
swanandx enabled auto-merge November 6, 2026 07:02
@swanandx

swanandx commented Nov 6, 2026

Copy link
Copy Markdown
Member

fixed the merge conflict in changelog, merging this

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx disabled auto-merge November 6, 2026 07:07
@swanandx
swanandx added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 64f6724 Nov 6, 2026
1 check passed

Uh oh!

There was an error while loading. Please reload this page.

@swanandx
swanandx deleted the jwks-serve-stale branch November 6, 2026 08:10
@blp

blp commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

fixed the merge conflict in changelog, merging this

Thank you!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

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

Reviewers

@swanandx swanandx swanandx approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@blp blp

Labels

authentication Everything related to OIDC, AWS Cognito, auth0, tenants, issuers etc. bug Something isn't working Pipeline manager Pipeline manager (API, API server, runner, compiler server) rust Pull requests that update Rust code

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@blp @swanandx
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.