fix(cache): gate the query cache write on a lease to reject stale lists#905
Conversation
getDocument() adopted the cache 3.1.0 Leasable primitive to close the cache-aside read-after-write race, but the withCache() query-cache helper still saved unconditionally. A list read that completed its database read just before a concurrent create/update/delete purged the query key would re-cache the now-stale list after the purge, re-poisoning exactly the race the lease was meant to kill. Capture getGeneration($key) before the callback runs its read and replace the unconditional save() with saveWithLease(), mirroring getDocument(): a purge between read and save advances the generation and the stale write is rejected. getGeneration degrades to '0' on cache failure, and non-Leasable adapters fall back to an unconditional save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds ChangesCache Lease Unit Tests
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds lease-gated writes for the query/list cache. The main changes are:
Confidence Score: 5/5The change is focused and merge-safe, with no review comments identified. The implementation matches the intended cache lease pattern and includes targeted regression coverage for stale query list writes and normal cache population.
What T-Rex did
Reviews (1): Last reviewed commit: "fix(cache): gate the query cache write o..." | Re-trigger Greptile |
The race
utopia-php/cache3.1.0 added the Leasable primitive (getGeneration()/saveWithLease(), PR #75/#77) to close the cache-aside read-after-write race: a reader that fetched a row from the database just before a concurrent purge must not re-populate the cache with the now-stale value after the purge has run.Database::getDocument()adopted it (PR #904feat/leasable-getdocument): it captures the generation before the database read and usessaveWithLease(...)on save, so a purge that lands in between rejects the stale write.The query/list cache added in PR #894 (
feat/cached-find) was left unprotected.withCache()reads through the cache, runs the callback (the database read), then saves unconditionally. If a concurrent create/update/delete callspurgeCachedQueries()between the callback's read and the save, the older request writes the stale list back into the cache after the purge — re-poisoning exactly the race the lease was meant to kill, this time for lists instead of single documents.The fix
In
withCache()(src/Database/Database.php):getGeneration($key)before the callback runs (wrapped in try/catch, degrading to'0'on cache failure) — placed after the optional rejected-value purge so the captured token reflects the generation as of the start of the database read.cache->save($key, $encoded, $hash)withcache->saveWithLease($key, $encoded, $hash, $generation)— a purge between read and save advances the generation and the now-stale list write is rejected.This mirrors the
getDocument()reference pattern exactly. Non-Leasable adapters (e.g. theMemorycache adapter) fall back to an unconditional save insideCache, so behaviour is unchanged for them.Writers fixed
withCache()query/list cachesrc/Database/Database.php:8808cache->save(...)(unconditional)cache->saveWithLease(..., $generation)(lease-gated)getDocument()(src/Database/Database.php:4946) was already lease-gated by PR #904 and is unchanged. The storage file-content cache in downstream consumers (Appwriteapp/controllers/shared/api.php) is a Filesystem-backed HTTP response cache with its own invalidation log — not a database list cache — and is out of scope.Tests
New
tests/unit/WithCacheLeaseTest.phpwith a self-contained Leasable in-memory cache adapter (the shippedMemoryadapter is intentionally non-Leasable, so it could not exercise the lease):testStaleListWriteAfterConcurrentPurgeIsRejected— the callback purges the query key mid-read; asserts the stale list is not re-cached. Fails without the fix (the old list lands in the cache after the purge), passes with it.testListWriteLandsWhenNoConcurrentPurge— with no concurrent purge, the list is cached as normal.Verification
src+tests): no errorssave()and passes withsaveWithLease()Dependency / propagation
The cache constraint is already
utopia-php/cache: 3.1.*andcomposer.lockalready resolves3.1.0, which providesgetGeneration/saveWithLease— no constraint bump needed. This fix lives inutopia-php/database, so it needs a new database release and a downstreamcomposer update(Appwrite/Cloud/Edge) to take effect for list endpoints.🤖 Generated with Claude Code
Summary by CodeRabbit