Skip to content

[6.x] Fix GraphQL and REST API response caches missing keys on invalidation under concurrent requests - #15747

Merged
jasonvarga merged 3 commits into
6.xfrom
graphql-cache-generation
Oct 30, 2026
Merged

jasonvarga merged 3 commits into
6.xfrom
graphql-cache-generation

Conversation

@duncanmcclean

@duncanmcclean duncanmcclean commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

This pull request fixes an issue where cached GraphQL and REST API responses could survive a content event (like EntrySaved) and keep being served until they expired, when they were cached by concurrent requests.

This was happening because GraphQL's DefaultCache and the REST API's DefaultCacher invalidate by forgetting the keys they've recorded under gql-cache:tracked-responses and api-cache:tracked-responses. Those lists are updated with a non-atomic read-modify-write, so concurrent requests overwrite each other's copy of the list and some keys are never recorded.

This PR fixes it by keying responses under a "generation" (a random string stored at gql-cache:generation / api-cache:generation). Invalidating now forgets the generation key, which is a single atomic operation, so every existing response becomes unreachable whether it was tracked or not.

The generation is read once per request, so get() and put() use the same one. This means a response computed before a content event lands in the old generation, rather than being served as stale in the new one. It's memoized per request rather than per instance because the REST cacher is held for the app's lifetime by ApiCacheManager, so a per-instance generation would go stale in long-lived processes. GraphQL uses the same approach for consistency.

The tracked lists are still kept, so old responses are cleaned up eagerly and anyone extending either class isn't broken. They're no longer responsible for correctness though.

Fixes #15523

duncanmcclean and others added 3 commits October 29, 2026 10:12
… can't escape invalidation

The generation is memoized per request rather than per instance, since the REST cacher is resolved once per app by ApiCacheManager and would otherwise hold a stale generation in long-lived processes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with the rest api cacher

This is purely for consistency, not necessity. The GraphQL cache is bound with bind() so each resolution is already a fresh instance, but the REST cacher is held for the app's lifetime by ApiCacheManager and has to memoize per request. Using the same approach in both avoids confusion about why they differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasonvarga jasonvarga changed the title [6.x] Fix GraphQL response cache missing keys on invalidation under concurrent requests [6.x] Fix GraphQL and REST API response caches missing keys on invalidation under concurrent requests Oct 30, 2026
@jasonvarga
jasonvarga merged commit 4e4e654 into 6.x Oct 30, 2026
65 checks passed
@jasonvarga
jasonvarga deleted the graphql-cache-generation branch October 30, 2026 05:29
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.

GraphQL response cache can miss keys on invalidation under concurrent requests

2 participants