Skip to content

feat: add opt-in opportunistic pre-fetch cache for clients and client grants#1571

Open
emsearcy wants to merge 3 commits into
auth0:mainfrom
emsearcy:feat/opportunistic-prefetch
Open

feat: add opt-in opportunistic pre-fetch cache for clients and client grants#1571
emsearcy wants to merge 3 commits into
auth0:mainfrom
emsearcy:feat/opportunistic-prefetch

Conversation

@emsearcy

@emsearcy emsearcy commented May 8, 2026

Copy link
Copy Markdown

🔧 Changes

Add an opt-in opportunistic pre-fetch cache for auth0_client and auth0_client_grant resource reads.

Large Auth0 deployments (100+ clients, 100+ grants) incur excessive latency and rate-limit exhaustion (especially non-production tenants!) during terraform plan CI runs because the provider fetches each resource individually. When pre-fetch is enabled, any individual resource read:

  1. Checks an in-memory cache (by resource type + ID).
  2. On a cache miss (while pages remain): fetches the next page of resources (50/page), stores all results, advances the page cursor.
  3. Re-checks the cache — returns the entry if now found.
  4. Falls back to a single-resource fetch if still not found.

This balances latency (never waits for all pages) with opportunistic bulk loading.

Opt-in:

provider "auth0" {
  prefetch = true  # or AUTH0_PREFETCH=true
}

The feature is off by default — existing users are unaffected.

Files changed:

File Change
internal/prefetch/cache.go Thread-safe sync.RWMutex-backed cache; page-cursor tracking; per-type fetch mutex (prevents TOCTOU races across parallel goroutines); atomic hit/miss/page counters
internal/prefetch/prefetch.go GetClient and GetClientGrant — opportunistic heuristic + fallback
internal/config/config.go prefetchCache field, GetPrefetchCache() accessor, wired in ConfigureProvider
internal/provider/provider.go prefetch bool schema attribute + AUTH0_PREFETCH env var
internal/auth0/client/resource.go Use prefetch path in readClient when cache non-nil
internal/auth0/client/resource_grant.go Use prefetch path in readClientGrant when cache non-nil
internal/auth0/client/resource_credentials.go Use prefetch path in readClientCredentials when cache non-nil
docs/guides/opportunistic_prefetch.md Usage guide
internal/prefetch/cache_test.go Unit tests

Known limitations:

  • Per-process cache only. OpenTofu/Terraform spawns multiple provider processes for parallelism. Each process has its own independent in-memory cache.
  • Cache is not invalidated. Populated once per provider process lifetime — correct for terraform plan but pre-fetch should not be used if Auth0 state changes mid-run.
  • Page size is fixed at 50, matching the SDK default.

Observability: At TF_LOG=DEBUG, a summary line is emitted when each resource type's page list is exhausted:

prefetch: client_grant cache exhausted  pages_fetched=3 cached=114 hits=68 misses=4 hit_rate_pct=94.4

📚 References

🔬 Testing

Unit tests in internal/prefetch/cache_test.go cover concurrent access, TOCTOU safety, pagination exhaustion, and fallback behavior.

Manual timing against a non-production Auth0 tenant with ~270 clients and ~165 grants:

Run Wall time
Upstream (no prefetch) 4 min 20 sec
AUTH0_PREFETCH=true 49 sec
Speedup ~5.3×

Cache stats from the prefetch run:

  • auth0_client: hit_rate=95.8% (264 cached, 6 pages fetched)
  • auth0_client_grant: hit_rate=100.0% (114 cached, 3 pages fetched)

📝 Checklist

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

emsearcy added 3 commits May 8, 2026 10:10
Add an opt-in pre-fetch mode (prefetch = true / AUTH0_PREFETCH=true) that
batches auth0_client and auth0_client_grant reads using page-based list
endpoints, reducing API call count for large deployments.

- internal/prefetch: new package with RWMutex-backed cache and GetClient/
  GetClientGrant heuristic functions
- internal/config: add *prefetch.Cache field wired in ConfigureProvider
- internal/provider: add prefetch schema attribute
- internal/auth0/client: use prefetch path in readClient,
  readClientGrant, and readClientCredentials when cache is non-nil
- docs/guides/opportunistic_prefetch.md: usage guide

Assisted-by: github-copilot:claude-sonnet-4.6
Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
…cache

Add atomic hit, miss, and pages-fetched counters per resource type to
the prefetch cache. A Summary() method exposes the current counts.

GetClient and GetClientGrant now emit a tflog.Debug line after each
page fetch, reporting the page number, item count, has-more flag, and
running cache hit/miss/pages-fetched totals. These are visible at
TF_LOG=DEBUG and allow operators to evaluate pre-fetch efficiency.

Assisted-by: github-copilot:claude-sonnet-4.6
Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Three fixes based on live testing:

1. TOCTOU — add a per-resource-type fetch mutex (fetchMu). Only one
   goroutine fetches a page at a time; others block and then get cache
   hits after the mutex is released. Double-check pattern re-checks the
   cache immediately after acquiring the lock, and again after the fetch.
   A separate countersMu avoids a deadlock that would arise from calling
   getOrInitCounters while holding the main mu write lock.

2. Pagination — pass IncludeTotals(true) to all list calls so that
   HasNext() can use Total > Start+Limit as a reliable termination
   signal rather than depending solely on the Next cursor string.

3. Observability — per-page log lines demoted to tflog.Trace; a single
   summary line (pages_fetched, cached, hits, misses, hit_rate_pct) is
   emitted at tflog.Debug when the cache transitions to exhausted. The
   cached counter is accumulated in setEntries and exposed via Summary.
   HitRate() helper added to Summary.

Assisted-by: github-copilot:claude-sonnet-4.6
Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
@emsearcy
emsearcy requested a review from a team as a code owner May 8, 2026 17:59
if !cache.isExhausted(resourceTypeClient) {
page := cache.nextPage(resourceTypeClient)

list, err := api.Client.List(ctx,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

List results may not be equivalent to read results. readClient and readClientCredentials now flatten objects returned from Client.List, not Client.Read. Auth0 documents special field/scope behavior for client fields like client_authentication_methods, client_secret,encryption_key, and signing keys; using list responses as read substitutes risks missing fields or producing diffs.


This approach balances latency (the provider never waits for all pages before returning) with
opportunistic bulk loading across a `terraform plan` run.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The cache used seems to be currently process-wide and never invalidated.
updateClient, updateClientGrant, and updateClientCredentials all call read afterward; if the object was cached before the write, state can be flattened from stale data. Clean call out that “Cache is not invalidated”, but the provider option affects apply too.

@@ -0,0 +1,225 @@
package prefetch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These tests are essential, but are mostly cache-unit tests and not provider behaviour tests. They focus on cache state/concurrency and does not validate read-vs-list equivalence, stale-cache-after-update, or integration behaviour.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants