1616use Utopia \Database \Attribute ;
1717use Utopia \Database \Cache \Epoch ;
1818use Utopia \Database \Cache \Owners ;
19+ use Utopia \Database \Cache \Registration ;
1920use Utopia \Database \Cache \Scope ;
2021use Utopia \Database \Capability ;
2122use Utopia \Database \Change ;
@@ -3545,7 +3546,9 @@ private function purgeCommittedCollection(string $collection): void
35453546 }
35463547
35473548 /**
3548- * Purge a document's cache slot, and once more after the open invalidation scope ends.
3549+ * Purge a document's cache slot, and once more after the open invalidation scope ends. A write whose purge
3550+ * cannot be confirmed blocks its collection's epoch instead, before it commits, so the document cached
3551+ * before it is never served again; only when that fails too is the write refused.
35493552 *
35503553 * @throws Exception
35513554 */
@@ -3575,7 +3578,20 @@ protected function purgeCachedDocumentInternal(string $collectionId, ?string $id
35753578 );
35763579 }
35773580
3578- $ this ->withRetries (fn () => $ this ->cache ->purge ($ documentKey ));
3581+ $ scoped = $ collectionId !== self ::METADATA && isset ($ this ->documentCacheMutations [$ context ]);
3582+ try {
3583+ if ($ scoped && isset ($ this ->documentCacheMutations [$ context ][$ collectionKey ])) {
3584+ $ this ->cache ->purge ($ documentKey );
3585+ } else {
3586+ $ this ->withRetries (fn () => $ this ->cache ->purge ($ documentKey ));
3587+ }
3588+ } catch (Throwable $ error ) {
3589+ if (! $ scoped ) {
3590+ throw $ error ;
3591+ }
3592+
3593+ $ this ->advanceDocumentCacheEpoch ($ collectionKey , $ this ->getDefinitionCacheKey ($ collectionId ));
3594+ }
35793595
35803596 return true ;
35813597 }
@@ -3619,7 +3635,7 @@ private function purgeCachedDefinitions(): void
36193635 * Purge the documents a transaction wrote once it has committed or rolled back. A reader
36203636 * outside the transaction may have cached the pre-commit row after the purge inside it, so
36213637 * when this purge fails the collection's epoch is retired instead, which no such fill survives.
3622- * A collection definition has no epoch of its own: its purge is tried once more .
3638+ * A collection definition has no epoch of its own, so its failed purge is the one thrown .
36233639 *
36243640 * @param array<string, string> $documents Collection keys by document key
36253641 */
@@ -3628,20 +3644,29 @@ protected function purgeWrittenDocuments(array $documents): void
36283644 $ definitions = $ this ->documentCacheDefinitions [$ this ->getEventContext ()] ?? [];
36293645 $ failure = null ;
36303646 $ retired = [];
3647+ $ reachable = true ;
36313648 foreach ($ documents as $ documentKey => $ collectionKey ) {
3649+ if (isset ($ retired [$ collectionKey ])) {
3650+ continue ;
3651+ }
36323652 try {
3633- $ this ->cache ->purge ($ documentKey );
3653+ if ($ reachable ) {
3654+ $ this ->withRetries (fn () => $ this ->cache ->purge ($ documentKey ));
3655+ } else {
3656+ $ this ->cache ->purge ($ documentKey );
3657+ }
36343658 } catch (Throwable $ error ) {
3635- $ failure ??= $ error ;
3659+ $ reachable = false ;
3660+ if (! isset ($ definitions [$ collectionKey ])) {
3661+ $ failure ??= $ error ;
3662+
3663+ continue ;
3664+ }
36363665 try {
3637- if (! isset ($ definitions [$ collectionKey ])) {
3638- $ this ->cache ->purge ($ documentKey );
3639- } elseif (! isset ($ retired [$ collectionKey ])) {
3640- $ retired [$ collectionKey ] = true ;
3641- $ this ->advanceDocumentCacheEpoch ($ collectionKey , $ definitions [$ collectionKey ]);
3642- }
3666+ $ this ->advanceDocumentCacheEpoch ($ collectionKey , $ definitions [$ collectionKey ]);
3667+ $ retired [$ collectionKey ] = true ;
36433668 } catch (Throwable ) {
3644- // The purge failure below reaches the caller either way.
3669+ $ failure ??= $ error ;
36453670 }
36463671 }
36473672 }
@@ -3838,14 +3863,7 @@ private function activateDocumentCacheEpoch(string $collectionKey, string $token
38383863 if ($ owner !== false && $ owner !== null && $ owner !== $ token ) {
38393864 throw new RuntimeException ("Invalid document cache owner ' {$ token }' for ' {$ collectionKey }' " );
38403865 }
3841- $ owned = $ owner === $ token ;
3842- if ($ owned && ! $ this ->cache ->purge ($ registration ->key , $ registration ->field )) {
3843- $ owner = $ this ->cache ->load ($ registration ->key , self ::TTL , $ registration ->field );
3844- if ($ owner !== false && $ owner !== null ) {
3845- throw new RuntimeException ("Failed to release document cache owner ' {$ token }' for ' {$ collectionKey }' " );
3846- }
3847- $ owned = false ;
3848- }
3866+ $ owned = $ owner === $ token && $ this ->releaseDocumentCacheOwner ($ registration , $ collectionKey , $ token );
38493867
38503868 $ startedKey = $ collectionKey .'#started ' ;
38513869 $ finishedKey = $ collectionKey .'#finished ' ;
@@ -3874,19 +3892,7 @@ private function activateDocumentCacheEpoch(string $collectionKey, string $token
38743892 return ;
38753893 }
38763894
3877- $ this ->cache ->purge ($ finishedKey );
3878- $ nextFinished = $ this ->cache ->getGeneration ($ finishedKey );
3879-
3880- // A cache flush restarts generations, so an unchanged #finished
3881- // only proves this purge was lost while the epoch read with it is
3882- // still in place; publish the new epoch after this check, not before.
3883- if (
3884- $ nextFinished === $ finished
3885- && \is_string ($ epoch )
3886- && $ this ->cache ->load ($ epochKey , self ::DOCUMENT_CACHE_PERMANENT ) === $ epoch
3887- ) {
3888- throw new RuntimeException ("Failed to finish document cache invalidation ' {$ epochKey }' " );
3889- }
3895+ $ nextFinished = $ this ->finishDocumentCacheWrite ($ collectionKey , $ finished , $ epoch );
38903896
38913897 $ nextStarted = $ this ->cache ->getGeneration ($ startedKey );
38923898 if ($ nextStarted === $ nextFinished ) {
@@ -3912,11 +3918,68 @@ private function publishDocumentCacheEpoch(string $collectionKey, string $starte
39123918 {
39133919 $ epochKey = $ collectionKey .'#epoch ' ;
39143920 $ epoch = self ::DOCUMENT_CACHE_ACTIVE_PREFIX .\bin2hex (\random_bytes (16 )).self ::DOCUMENT_CACHE_SEPARATOR .$ started ;
3915- if ($ this ->cache ->save ($ epochKey , $ epoch ) === false ) {
3916- throw new RuntimeException ("Failed to activate document cache epoch ' {$ epochKey }' " );
3917- }
3921+ $ this ->withRetries (function () use ($ epochKey , $ epoch ): void {
3922+ if ($ this ->cache ->save ($ epochKey , $ epoch ) === false ) {
3923+ throw new RuntimeException ("Failed to activate document cache epoch ' {$ epochKey }' " );
3924+ }
3925+ });
39183926
3919- $ this ->purgeCachedDefinition ($ definitionKey );
3927+ $ this ->withRetries (fn () => $ this ->purgeCachedDefinition ($ definitionKey ));
3928+ }
3929+
3930+ /**
3931+ * Release this write's registration, and report whether it was still this write's to release. A purge
3932+ * that cannot be confirmed is retried while the registration is still in place.
3933+ */
3934+ private function releaseDocumentCacheOwner (Registration $ registration , string $ collectionKey , string $ token ): bool
3935+ {
3936+ $ released = true ;
3937+ $ this ->withRetries (function () use ($ registration , $ collectionKey , $ token , &$ released ): void {
3938+ if ($ this ->cache ->purge ($ registration ->key , $ registration ->field )) {
3939+ return ;
3940+ }
3941+
3942+ $ owner = $ this ->cache ->load ($ registration ->key , self ::TTL , $ registration ->field );
3943+ if ($ owner !== false && $ owner !== null ) {
3944+ throw new RuntimeException ("Failed to release document cache owner ' {$ token }' for ' {$ collectionKey }' " );
3945+ }
3946+ $ released = false ;
3947+ });
3948+
3949+ return $ released ;
3950+ }
3951+
3952+ /**
3953+ * Count this write as finished and return the `#finished` generation after it. A cache flush restarts
3954+ * generations, so an unchanged `#finished` only proves the purge was lost while the epoch read with it
3955+ * is still in place; the purge is retried only then, so a purge that did land is never counted twice.
3956+ */
3957+ private function finishDocumentCacheWrite (string $ collectionKey , string $ finished , mixed $ epoch ): string
3958+ {
3959+ $ finishedKey = $ collectionKey .'#finished ' ;
3960+ $ epochKey = $ collectionKey .'#epoch ' ;
3961+ $ next = $ finished ;
3962+ $ attempted = false ;
3963+ $ this ->withRetries (function () use ($ finishedKey , $ epochKey , $ finished , $ epoch , &$ next , &$ attempted ): void {
3964+ if ($ attempted ) {
3965+ $ next = $ this ->cache ->getGeneration ($ finishedKey );
3966+ }
3967+ if ($ next === $ finished ) {
3968+ $ attempted = true ;
3969+ $ this ->cache ->purge ($ finishedKey );
3970+ }
3971+
3972+ $ next = $ this ->cache ->getGeneration ($ finishedKey );
3973+ if (
3974+ $ next === $ finished
3975+ && \is_string ($ epoch )
3976+ && $ this ->cache ->load ($ epochKey , self ::DOCUMENT_CACHE_PERMANENT ) === $ epoch
3977+ ) {
3978+ throw new RuntimeException ("Failed to finish document cache invalidation ' {$ epochKey }' " );
3979+ }
3980+ });
3981+
3982+ return $ next ;
39203983 }
39213984
39223985 /**
0 commit comments