feat: add opt-in opportunistic pre-fetch cache for clients and client grants#1571
feat: add opt-in opportunistic pre-fetch cache for clients and client grants#1571emsearcy wants to merge 3 commits into
Conversation
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>
| if !cache.isExhausted(resourceTypeClient) { | ||
| page := cache.nextPage(resourceTypeClient) | ||
|
|
||
| list, err := api.Client.List(ctx, |
There was a problem hiding this comment.
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. | ||
|
|
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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.
🔧 Changes
Add an opt-in opportunistic pre-fetch cache for
auth0_clientandauth0_client_grantresource reads.Large Auth0 deployments (100+ clients, 100+ grants) incur excessive latency and rate-limit exhaustion (especially non-production tenants!) during
terraform planCI runs because the provider fetches each resource individually. When pre-fetch is enabled, any individual resource read:This balances latency (never waits for all pages) with opportunistic bulk loading.
Opt-in:
The feature is off by default — existing users are unaffected.
Files changed:
internal/prefetch/cache.gosync.RWMutex-backed cache; page-cursor tracking; per-type fetch mutex (prevents TOCTOU races across parallel goroutines); atomic hit/miss/page countersinternal/prefetch/prefetch.goGetClientandGetClientGrant— opportunistic heuristic + fallbackinternal/config/config.goprefetchCachefield,GetPrefetchCache()accessor, wired inConfigureProviderinternal/provider/provider.goprefetchbool schema attribute +AUTH0_PREFETCHenv varinternal/auth0/client/resource.goreadClientwhen cache non-nilinternal/auth0/client/resource_grant.goreadClientGrantwhen cache non-nilinternal/auth0/client/resource_credentials.goreadClientCredentialswhen cache non-nildocs/guides/opportunistic_prefetch.mdinternal/prefetch/cache_test.goKnown limitations:
terraform planbut pre-fetch should not be used if Auth0 state changes mid-run.Observability: At
TF_LOG=DEBUG, a summary line is emitted when each resource type's page list is exhausted:📚 References
🔬 Testing
Unit tests in
internal/prefetch/cache_test.gocover concurrent access, TOCTOU safety, pagination exhaustion, and fallback behavior.Manual timing against a non-production Auth0 tenant with ~270 clients and ~165 grants:
AUTH0_PREFETCH=trueCache 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