From 0f44d314053b290c8d335f55f234286c522d1729 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Sat, 31 Oct 2026 00:54:07 +0000 Subject: [PATCH 1/2] fix(cache): preserve application list caches during rolling migration --- composer.json | 2 +- composer.lock | 14 +++++++------- src/Database/Cache/Bridge.php | 17 ++++++----------- tests/unit/Cache/BridgeTest.php | 20 ++++++++++++++++++++ 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 5aa60a082..14d7d38fa 100755 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "ext-redis": "*", "utopia-php/validators": "^1.0 || ^2.0", "utopia-php/console": "^0.1 || ^0.2", - "utopia-php/cache": "^6.1.1", + "utopia-php/cache": "^6.1.3", "utopia-php/pools": "^2.0", "utopia-php/mongo": "^1.5.4", "utopia-php/query": "^0.7.2", diff --git a/composer.lock b/composer.lock index 543465029..cfd9d71a3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5d299b8d0325e34aee41d67f7bd57ded", + "content-hash": "b7512f500bd39b9dfd1ae09d62503d71", "packages": [ { "name": "brick/math", @@ -2007,16 +2007,16 @@ }, { "name": "utopia-php/cache", - "version": "6.1.1", + "version": "6.1.3", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "8d72a5e91baaa11855cd0ebf6c8934b596f2a93a" + "reference": "3f1d4d1e53c6f686dabb8b9f37b31eac5ec7375f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/8d72a5e91baaa11855cd0ebf6c8934b596f2a93a", - "reference": "8d72a5e91baaa11855cd0ebf6c8934b596f2a93a", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/3f1d4d1e53c6f686dabb8b9f37b31eac5ec7375f", + "reference": "3f1d4d1e53c6f686dabb8b9f37b31eac5ec7375f", "shasum": "" }, "require": { @@ -2061,9 +2061,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/6.1.1" + "source": "https://github.com/utopia-php/cache/tree/6.1.3" }, - "time": "2026-10-30T23:38:33+00:00" + "time": "2026-10-31T00:38:42+00:00" }, { "name": "utopia-php/circuit-breaker", diff --git a/src/Database/Cache/Bridge.php b/src/Database/Cache/Bridge.php index 3a6af4bfc..d85e96699 100644 --- a/src/Database/Cache/Bridge.php +++ b/src/Database/Cache/Bridge.php @@ -86,12 +86,7 @@ public function loadMany(string $key, int $ttl, array $fields = []): array #[\Override] public function saveMany(string $key, array $data, int $ttl = 0): array|false { - // Database caching does not batch-fill during the legacy pass. - if ($this->mode === 'legacy') { - throw new Unavailable('Legacy migration does not support batch fills'); - } - - return $this->cache->saveMany('v2:'.$key, $data, $ttl); + return $this->reader()->saveMany($this->key($key), $data, $ttl); } /** @return array{version: string, value: mixed} */ @@ -138,18 +133,18 @@ function () use ($key, $hash, &$result): void { } /** Rotate caller-owned withCache regions in both keyspaces, including when one is cold. */ - public function rotate(string $key): void + public function rotate(string $key, int $ttl = 0): void { $this->both( - fn () => $this->rotateIn($this->legacy, $key), - fn () => $this->rotateIn($this->cache, 'v2:'.$key), + fn () => $this->rotateIn($this->legacy, $key, $ttl), + fn () => $this->rotateIn($this->cache, 'v2:'.$key, $ttl), ); } - private function rotateIn(Cache $cache, string $key): void + private function rotateIn(Cache $cache, string $key, int $ttl): void { $cache->purge($key); - if ($cache->save($key, \bin2hex(\random_bytes(16))) === false) { + if ($cache->save($key, \bin2hex(\random_bytes(16)), ttl: $ttl) === false) { throw new Unavailable('Failed to rotate cache region'); } } diff --git a/tests/unit/Cache/BridgeTest.php b/tests/unit/Cache/BridgeTest.php index 659fcfea5..13607dd21 100644 --- a/tests/unit/Cache/BridgeTest.php +++ b/tests/unit/Cache/BridgeTest.php @@ -118,6 +118,26 @@ public function testRapidWritesWithCloudsGraceWindowKeepBothProtocolsUsable(): v } } + public function testApplicationListCacheSupportsExpiringVersionsAndBatchFillsInEveryPhase(): void + { + foreach (['legacy', 'dual', 'v2'] as $mode) { + $cache = Bridge::wrap($this->cache, $mode); + $key = $this->namespace.':lists:'.$mode; + $this->assertSame('version', $cache->save($key.':version', 'version', ttl: 60)); + $this->assertSame('version', $cache->load($key.':version', 60)); + $wireKey = $mode === 'legacy' ? $key : 'v2:'.$key; + $this->assertGreaterThan(0, $this->redis->ttl($wireKey.':version')); + $cache->rotate($key.':version', 60); + $this->assertNotSame('version', $cache->load($key.':version', 60)); + $this->assertGreaterThan(0, $this->redis->ttl($wireKey.':version')); + $fields = ['documents' => ['row'], 'total' => '1']; + $this->assertSame($fields, $cache->saveMany($key, $fields)); + $this->assertSame($fields, $cache->loadMany($key, 60)); + $cache->purge($key); + $this->assertSame([], $cache->loadMany($key, 60)); + } + } + public function testSlotZeroRemainsCacheableAcrossEveryPhase(): void { foreach (['', 'legacy', 'dual', 'v2'] as $mode) { From 35a91ac0d3cf8958ec6bb92fe7d81ea1114e1461 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:01:00 +0000 Subject: [PATCH 2/2] test(cache): use the concrete bridge for region rotation --- tests/unit/Cache/BridgeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Cache/BridgeTest.php b/tests/unit/Cache/BridgeTest.php index 13607dd21..9c90990b7 100644 --- a/tests/unit/Cache/BridgeTest.php +++ b/tests/unit/Cache/BridgeTest.php @@ -121,7 +121,7 @@ public function testRapidWritesWithCloudsGraceWindowKeepBothProtocolsUsable(): v public function testApplicationListCacheSupportsExpiringVersionsAndBatchFillsInEveryPhase(): void { foreach (['legacy', 'dual', 'v2'] as $mode) { - $cache = Bridge::wrap($this->cache, $mode); + $cache = new Bridge($this->cache, $mode); $key = $this->namespace.':lists:'.$mode; $this->assertSame('version', $cache->save($key.':version', 'version', ttl: 60)); $this->assertSame('version', $cache->load($key.':version', 60));