Repository navigation
fix(documents): retire cached documents before a write the cache cannot confirm commits - #1042
Conversation
…ot confirm commits utopia-php/cache's circuit breaker now throws when it cannot confirm a purge. Single document writes already let that throw roll them back. Two paths still committed and then served the old cached documents once the cache recovered. Batch writes and purgeCachedCollection(): with the circuit open, the owner registration and the epoch read both take the breaker's fallback. blockDocumentCacheEpoch() read a missing epoch as nothing to invalidate and let the write go ahead. It now purges the unreadable epoch anyway. A real absence still returns false. An unconfirmed purge throws, so a batch write rolls back and purgeCachedCollection() reports the failure instead of returning as if it had purged. Schema changes: they purged the collection's documents only after the definition committed. A purge after the commit cannot be guaranteed: retrying or deferring it needs the same cache that just refused, and a lapsing marker written then fails the same way. So a write to a collection's definition now places the collection's tombstone inside its own transaction. If the cache refuses, the change rolls back through updateMetadata()'s existing rollbackOperation. Once the tombstone is stored, no fill made before the commit is served again. After the commit, activation publishes a fresh epoch, and a failed activation lapses into a new epoch after the writer timeout. The schema methods' own purges after the commit are therefore redundant for documents, and they now log a failure instead of throwing for a change that is stored. Query cache invalidation is unchanged. That adds a cache operation after the commit, so deleteCollection() and deleteRelationship() now check mayHaveCommitted() before rolling back. Otherwise a failure after the commit would recreate a dropped table, or reverse a relationship, whose metadata is already gone. What remains: a failure after the commit of the definition write is still thrown as a committed failure, as updateMetadata() documents. A reader that refilled the definition between the in-transaction purge and the commit can serve it for up to DOCUMENT_CACHE_RECHECK (60s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
Retires cached documents before a write the cache cannot confirm commits (utopia-php/database#1042), paired with the cache purge fix in appwrite#14642. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
appwrite/appwrite#14642 makes utopia-php/cache's circuit breaker throw
Unavailablewhen it cannot confirm a purge. Before it, the breaker returnedfalse. Single document writes already let a purge exception through, and it rolls them back. Two paths still committed, and then served the old cached documents once the cache recovered:purgeCachedCollection(). While the circuit is open, the owner registration (a save) returnsfalseand the epoch read misses.blockDocumentCacheEpoch()took a missing epoch to mean nothing was cached and let the write through. In fact the epoch is still in Redis.renameAttribute(),updateAttribute(), the index methods and so on purge the collection's documents only after the new definition has committed. The cache may refuse that purge, so the change stays and documents in the old shape are served.Fix
Unreadable epoch. When the owner registration fails and the epoch can't be read,
blockDocumentCacheEpoch()now purges the epoch key anyway. A purge the cache can't confirm throws: a batch write rolls back, andpurgeCachedCollection()reports the failure instead of returning as if it had purged. A real absence returnsfalseexactly as before.purgeCachedCollection()after a schema change. I looked at three options:Only the last one holds: the marker has to be in place before the change commits. A write to a collection's definition in
_metadata(updateMetadata(),deleteCollection(), the relationship metadata transactions) now places the collection's document tombstone inside its own transaction, next to the definition purge.rollbackOperation. You get an error, but nothing is committed.So the purges the schema methods run after commit (now
purgeCommittedCollection()) are redundant for documents. If one fails, the failure is logged instead of thrown for a change that is already stored. Query-cache invalidation keeps its existing behaviour. The relationship-update paths whose purge failure is meant to trigger restores (RelationshipSchemaTest::testAFailedJunctionPurgeRestores…) are left as they were.The new activation is one more cache operation after commit.
deleteCollection()anddeleteRelationship()used to roll back on anyThrowable, so a failure after commit could have recreated a dropped table, or reversed a relationship, whose metadata was already gone. They now checkmayHaveCommitted()first, likecreateCollection()andcreateRelationship()already do.Known limits:
updateMetadata()documents.DOCUMENT_CACHE_RECHECK(60s). Documents have no such window.Before / after
With the cache behind the throwing breaker,
Tests\Unit\Documents\CacheUnavailableTest:main(8.3.9)purgeCachedCollection(), circuit openOn
mainthe three bold rows fail (testAWriteTheCacheCannotInvalidateIsRolledBack@batch update, open,testPurgeCachedCollectionReportsACacheThatCannotConfirmIt,testASchemaChangeRetiresCachedDocumentsBeforeItCommits), and they pass here. The other rows are guards for paths that already used the throw.Checks: Pint passes; PHPStan passes with both configs. The unit suite passes apart from
BaseAdapterStateTest::testAKeyThePatternEngineCannotFilterIsAnErrorandSwooleAbsentTest, which fail the same way onmainon my machine because it lacks ext-redis and uses a Swoole build without the extension.🤖 Generated with Claude Code
Summary by CodeRabbit