Skip to content

feat(cache): shared single-flight credential cache, wired into OIDC backend auth#61

Open
alukach wants to merge 2 commits into
mainfrom
feat/credential-cache
Open

feat(cache): shared single-flight credential cache, wired into OIDC backend auth#61
alukach wants to merge 2 commits into
mainfrom
feat/credential-cache

Conversation

@alukach
Copy link
Copy Markdown
Member

@alukach alukach commented Jun 3, 2026

Refs #56 (roadmap item 2: caching credential provider).

Rebased onto main (#57 has landed, so the federation crate is now multistore-backend-federation).

What

Adds a shared, single-flight, proactively-refreshing credential cache and wires the OIDC backend-auth hot path onto it — replacing the simpler bespoke cache that previously lived in multistore-oidc-provider.

Two commits:

  1. feat — the CredentialCache primitive (single-flight + proactive refresh).
  2. refactor — extract it into a shared multistore-credential-cache crate, generalize it over any Expiring credential type, and rewire oidc-provider onto it.

The cache

Short-lived credentials are expensive to mint, so re-minting on every request hammers the issuing STS and adds latency. CredentialCache<T> caches the current value per credential identity (an opaque key the caller chooses — e.g. a role ARN or the rendered OIDC subject) and:

  • serves a cached value while it's still comfortably valid,
  • proactively refreshes once it's within a configurable refresh_lead of expiry (so a credential never expires mid-use), and
  • single-flights refreshes: concurrent callers for the same key await one in-flight fetch (per-key futures::lock::Mutex) rather than each launching their own.
let cache = CredentialCache::new(Duration::minutes(5)); // or CredentialCache::default()
let creds = cache.get_or_fetch(role_arn, now, || async { exchange().await }).await?;
cache.invalidate(role_arn); // drop-and-refetch, e.g. after a backend 403

It's generic over any T: Expiring (with a blanket impl for Arc<T>), so both FederatedCredentials (AWS STS, in backend-federation) and oidc-provider's CloudCredentials (multi-cloud) share one implementation. Closure-based get_or_fetch keeps it flexible — the caller supplies the fetch, and the cache owns dedup + refresh.

Consolidation

Before this PR there were two credential caches for one concept, and the one actually on the hot path was the weaker one:

new shared cache old oidc-provider cache (removed)
API get_or_fetch closure get / put
Single-flight ❌ (thundering herd on a cold cache)
Refresh proactive lead window fixed 60s margin
Clock injected now (runtime-agnostic) Utc::now() inside the cache

oidc-provider's get_credentials now goes through get_or_fetch, gaining single-flight + proactive refresh. The clock is injected at the one production call site (backend_auth), consistent with the rest of the wasm-path crates.

The cache lives in its own multistore-credential-cache crate (rather than core) to keep the runtime-agnostic core uncoupled and match the workspace's small-focused-crate layout.

Runtime-agnostic

  • Takes now as a parameter instead of reading a clock (Utc::now() isn't available on wasm32-unknown-unknown without extra features).
  • Uses futures::lock::Mutex, so it needs no async runtime (tokio is dev-only, for the concurrency test).
  • &self throughout; cheap to share behind an Arc.

Docs

New Architecture → Caching page (docs/architecture/caching.md): the in-memory (per-isolate) / Cloudflare Cache API (per-colo) / Workers KV (global) / Durable Object cache tiers, the layering pattern (Cache API as an L2 inside the fetch closure), and security best practices for an external credential cache. Added to the sidebar and cross-linked from Backend Auth.

Tests / verification

  • multistore-credential-cache: 6 unit tests (miss / hit-while-fresh / refresh-within-lead / invalidate / key-isolation / concurrent single_flights_concurrent_fetches) + doctest.
  • multistore-oidc-provider: existing 16 tests pass against the rewired cache.
  • Native build + wasm checks (cf-workers crate and the example) clean; cargo fmt --check + clippy clean; VitePress docs site builds with valid links.

Scope note

Proactive refresh is single-flighted and synchronous: the caller that crosses the lead window awaits the refresh; concurrent callers await the per-key lock and then see the fresh value. True background refresh-ahead — return the still-valid credential instantly and refresh off-task — needs runtime task spawning (tokio::spawn vs wasm_bindgen_futures::spawn_local), so it's intentionally left as a follow-up rather than pulling a spawn abstraction into the crate now. Cross-isolate / cross-colo sharing (Cache API / KV / Durable Objects) is documented as a layering pattern but not implemented here.

@github-actions github-actions Bot added the feat label Jun 3, 2026
Base automatically changed from feat/backend-federation to main June 3, 2026 21:35
…ive refresh)

Roadmap item 2 of #56. Caches short-lived FederatedCredentials per credential
identity so a proxy doesn't re-mint on every request:

- proactive refresh once within a configurable lead window of expiry (so a
  credential never expires mid-use),
- single-flight: concurrent callers for the same key await one in-flight fetch
  via a per-key futures::lock::Mutex,
- runtime-agnostic: the caller passes `now` (no clock dep) and no async runtime
  is required.

Closure-based get_or_fetch(key, now, fetch) keeps it flexible; invalidate(key)
supports drop-and-refetch on backend rejection. Tested: miss/hit/refresh/
invalidate/key-isolation and concurrent single-flight.

Rebased onto main: the federation crate landed on main as backend-federation
(#57), so this folds the cache into that crate instead of re-adding a duplicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

🚀 Latest commit deployed to https://multistore-proxy-pr-61.development-seed.workers.dev

  • Date: 2026-06-04T05:45:39Z
  • Commit: b65517f

…onto it

PR #61 added a single-flight, proactively-refreshing CredentialCache to
backend-federation, but oidc-provider already had its own simpler get/put
cache (no single-flight, calls Utc::now() directly). Two caches for one
concept, and the weaker one was the one actually on the hot path.

Consolidate onto one implementation:

- New crate `multistore-credential-cache` holds a generic `CredentialCache<T>`
  over any `T: Expiring`, plus the `Expiring` trait (with a blanket impl for
  `Arc<T>`). This is where #61's single-flight + proactive-refresh + closure-
  based get_or_fetch primitive now lives, generalized off the concrete
  FederatedCredentials type.
- backend-federation drops its own cache module and just implements
  `Expiring` for FederatedCredentials.
- oidc-provider deletes its bespoke cache and rewires `get_credentials` onto
  `get_or_fetch`, gaining single-flight and proactive refresh for free. The
  clock is now injected (a `now` param) instead of `Utc::now()` inside the
  cache, keeping the cache runtime-agnostic; backend_auth supplies it at the
  one production call site (consistent with the rest of the wasm-path crates).

Also document caching across runtimes: new docs/architecture/caching.md covers
the in-memory (per-isolate) / Cache API (per-colo) / KV (global) / Durable
Object tiers, the layering pattern, and external-cache security best practices;
backend-auth.md links to it.

Verified: all affected crates' tests pass; native build, wasm checks
(cf-workers + example), fmt, and clippy clean; docs site builds (links valid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

📖 Docs preview deployed to https://multistore-docs-pr-61.development-seed.workers.dev

  • Date: 2026-06-04T05:45:39Z
  • Commit: b65517f

@alukach alukach changed the title feat(federation): add CredentialCache (single-flight + proactive refresh) feat(cache): shared single-flight credential cache, wired into OIDC backend auth Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant