Skip to content

fix(introspection): coalesce concurrent cache misses - #575

Merged
kevinlzw merged 3 commits into
zmartzone:masterfrom
shreemaan-abhishek:fix/coalesce-introspection-cache-misses
Oct 9, 2026
Merged

kevinlzw merged 3 commits into
zmartzone:masterfrom
shreemaan-abhishek:fix/coalesce-introspection-cache-misses

Conversation

@shreemaan-abhishek

@shreemaan-abhishek shreemaan-abhishek commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • coalesce concurrent introspection cache misses for the same token within an NGINX instance
  • share cacheable, non-cacheable, and failed introspection results with requests waiting on the same lock generation
  • treat the per-token lock as a bounded lease that must outlive the introspection request
  • recheck the cache and fall back to direct introspection when coordination times out or fails
  • keep cache hits lock-free and preserve explicit introspection_cache_ignore behavior
  • document the coordination options and cover concurrency, bypass, failure, timeout, and lease behavior

Closes #573.

Checklist

  • Tests pass with docker build -f tests/Dockerfile . -t lua-resty-openidc/test
  • Tests pass with docker run -t --rm lua-resty-openidc/test:latest
  • ChangeLog is updated for behavior changes, bug fixes, features, or user-visible documentation changes

@shreemaan-abhishek

shreemaan-abhishek commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

@kevinlzw could you please review this PR?

@kevinlzw kevinlzw left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry for late, please check below comments.

@kevinlzw

Copy link
Copy Markdown
Collaborator

The owner token does not make the release generation-safe because dict:get() and dict:delete() are separate shared-dict operations.
A possible interleaving is:

  1. Worker A reads the lock and sees its own owner token.
  2. A's lease expires and worker B acquires the same lock key for the next generation.
  3. Worker A calls delete() and removes B's lock.
    A third worker can then acquire the lock and issue another introspection request while B is still in flight. ngx.shared.DICT does not provide an atomic compare-and-delete primitive, so I do not think this implementation can safely support an owner continuing after its lock lease has expired.
    Could we instead treat the lock as a lease, require introspection_lock_exptime to exceed the bounded introspection request duration, and avoid the “outlasted generation” contract? That would make the locking model much easier to reason about.

@kevinlzw

Copy link
Copy Markdown
Collaborator

I think cache-miss coalescing should remain a transparent optimization. At the moment, if lock acquisition times out, this returns an error without attempting introspection. With the default five-second timeout, a temporarily slow IdP can therefore turn otherwise valid tokens into authentication failures for all waiting requests.
This changes the existing availability behavior: before this PR, those requests would each perform introspection and could still succeed.
Could lock timeout/failure re-check the cache and then fall back to the existing direct introspection path by default? A strict fail-closed mode could be added later as an explicit opt-in, but it should not be the default behavior.

@kevinlzw

Copy link
Copy Markdown
Collaborator

The generation-scoped temporary-result relay is doing a lot of work to share endpoint failures and non-cacheable responses. It adds a fairly complex state machine around a cache-miss optimization and also changes failure/retry behavior.
Would it be reasonable to scope the first version to cacheable successful results only? The standard flow would then be: cache lookup, acquire lease, cache lookup again, introspect once, populate the cache, release. For failed or non-cacheable responses, releasing the lease and preserving the existing direct-introspection behavior would be simpler and more backward-compatible.

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

For the temporary-result relay, the motivating APISIX case is an overloaded introspection endpoint returning 503. Limiting coordination to cacheable successes means every waiter eventually retries introspection, so the overload scenario remains unresolved. Would you be open to retaining in-flight sharing for failed and non-cacheable results after the lease and fallback changes, or would you prefer this PR to address only cacheable successes and defer that behavior?

@kevinlzw

Copy link
Copy Markdown
Collaborator

Thanks for the update. The fallback change addresses my main compatibility concern. Your explanation makes sense: limiting coordination to cacheable successes would leave the original 503 overload scenario unresolved, so I’m happy to retain in-flight sharing for failed and non-cacheable results.

A few focused changes would still help:

  • Let waiters consume the result of the generation they originally observed without first acquiring the lock or switching to a newer generation.
  • Restore the owner check before releasing the lock as a defensive measure, while documenting that it is not atomic and does not fully eliminate the race.
  • Add multi-worker tests covering lease expiry and coordination-state eviction. Duplicate upstream calls may occur when coordination breaks down, but coordination failure alone should not cause authentication failures.

I’m comfortable treating coalescing as a best-effort optimization with these limitations documented. Happy to discuss the trade-offs further.

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

Implemented all three follow-ups in 7784c0e:

  • Waiters retain the first lock generation they observe and check, in order, the regular introspection cache and that generation's temporary result before attempting to acquire the lock. They no longer switch to later observed generations or relay a completed result through a newly acquired generation.
  • Lock release now checks that the shared-dict value still matches the caller's owner token before deleting it. The comment explicitly documents that get plus delete is not atomic and only narrows the race window.
  • Added multi-worker coverage (four nginx workers) for both lease expiry and forced coordination-state eviction. The tests verify that multiple workers participate, duplicate upstream calls are tolerated after coordination breaks down, and every client request still receives the successful introspection result rather than an authentication failure.

Validation:

  • Focused introspection suite: 93 successes, 0 failures
  • Full suite: 597 successes, 0 failures, 0 errors, 1 pre-existing pending test

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

@kevinlzw do you have any further comments?

@kevinlzw

kevinlzw commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

@shreemaan-abhishek
Sorry that I missed this, let me check again tonight.

@kevinlzw kevinlzw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for addressing the follow-ups. The latest changes address my concerns within the agreed best-effort coalescing model, with direct introspection as the fallback when coordination fails. LGTM.

@kevinlzw
kevinlzw merged commit 4ebdc50 into zmartzone:master Oct 9, 2026
@kevinlzw

kevinlzw commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Hi @shreemaan-abhishek ,

I need to get a new api key to release in luarocks, so please wait for sometime. Thank you!

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

@kevinlzw thank you so much for the prompt responses and for the release plan too... I was hoping I would raise another PR to fix this issue: #574... could you plan the release after the second fix? Thank you so much, you are awesome.

@kevinlzw

kevinlzw commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Hi @shreemaan-abhishek ,
Thank you. I will release this version and release another minor release after you raise another PR. Reason: I have already published the release in GitHub.

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.

Concurrent introspection cache misses cause duplicate endpoint requests

2 participants