From bf6e995b30b4f25e2aea00a40021a80c17780f9c Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 17:25:03 +0000 Subject: [PATCH 1/8] Upgrade cache dependency for reconnection support --- composer.json | 4 ++-- composer.lock | 49 +++++++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/composer.json b/composer.json index 9717c6434..2d6df38fe 100755 --- a/composer.json +++ b/composer.json @@ -38,8 +38,8 @@ "ext-mongodb": "*", "ext-mbstring": "*", "utopia-php/framework": "0.33.*", - "utopia-php/cache": "0.13.*", - "utopia-php/pools": "0.8.*", + "utopia-php/cache": "1.0.*", + "utopia-php/pools": "1.0.*", "utopia-php/mongo": "0.11.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 9b21d4196..a9b5f100f 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": "1ef51fb364b438b677b5bce17887153c", + "content-hash": "b16f905a54fc178bd3a93e89c9f8cf27", "packages": [ { "name": "brick/math", @@ -1383,16 +1383,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "d63c23357d74715a589454c141c843f0172bec6c" + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c", - "reference": "d63c23357d74715a589454c141c843f0172bec6c", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", "shasum": "" }, "require": { @@ -1460,7 +1460,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.4" + "source": "https://github.com/symfony/http-client/tree/v7.4.5" }, "funding": [ { @@ -1480,7 +1480,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T16:34:22+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-client-contracts", @@ -2026,16 +2026,16 @@ }, { "name": "utopia-php/cache", - "version": "0.13.2", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "5768498c9f451482f0bf3eede4d6452ddcd4a0f6" + "reference": "7068870c086a6aea16173563a26b93ef3e408439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/5768498c9f451482f0bf3eede4d6452ddcd4a0f6", - "reference": "5768498c9f451482f0bf3eede4d6452ddcd4a0f6", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/7068870c086a6aea16173563a26b93ef3e408439", + "reference": "7068870c086a6aea16173563a26b93ef3e408439", "shasum": "" }, "require": { @@ -2043,7 +2043,7 @@ "ext-memcached": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/pools": "0.8.*", + "utopia-php/pools": "1.*", "utopia-php/telemetry": "*" }, "require-dev": { @@ -2072,9 +2072,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.13.2" + "source": "https://github.com/utopia-php/cache/tree/1.0.0" }, - "time": "2025-12-17T08:55:43+00:00" + "time": "2026-01-28T10:55:44+00:00" }, { "name": "utopia-php/compression", @@ -2233,16 +2233,16 @@ }, { "name": "utopia-php/pools", - "version": "0.8.3", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/pools.git", - "reference": "ad7d6ba946376e81c603204285ce9a674b6502b8" + "reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/pools/zipball/ad7d6ba946376e81c603204285ce9a674b6502b8", - "reference": "ad7d6ba946376e81c603204285ce9a674b6502b8", + "url": "https://api.github.com/repos/utopia-php/pools/zipball/b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1", + "reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1", "shasum": "" }, "require": { @@ -2252,7 +2252,8 @@ "require-dev": { "laravel/pint": "1.*", "phpstan/phpstan": "1.*", - "phpunit/phpunit": "11.*" + "phpunit/phpunit": "11.*", + "swoole/ide-helper": "6.*" }, "type": "library", "autoload": { @@ -2279,9 +2280,9 @@ ], "support": { "issues": "https://github.com/utopia-php/pools/issues", - "source": "https://github.com/utopia-php/pools/tree/0.8.3" + "source": "https://github.com/utopia-php/pools/tree/1.0.2" }, - "time": "2025-12-17T09:35:18+00:00" + "time": "2026-01-28T13:12:36+00:00" }, { "name": "utopia-php/telemetry", @@ -4520,7 +4521,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4529,6 +4530,6 @@ "ext-mongodb": "*", "ext-mbstring": "*" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } From 2481a622a642811ab87e2b3e6316b2eb32799595 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 18:02:36 +0000 Subject: [PATCH 2/8] Update PHP requirement to 8.4+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2d6df38fe..341403b59 100755 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, "require": { - "php": ">=8.1", + "php": ">=8.4", "ext-pdo": "*", "ext-mongodb": "*", "ext-mbstring": "*", From 27ae55c57cf16e675326b4cd321658ef1214ece6 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 29 Jan 2026 09:22:59 +0000 Subject: [PATCH 3/8] Fix Pool constructor for pools 1.0 compatibility --- tests/e2e/Adapter/PoolTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/PoolTest.php b/tests/e2e/Adapter/PoolTest.php index 42bea2c68..8f5f2bd3a 100644 --- a/tests/e2e/Adapter/PoolTest.php +++ b/tests/e2e/Adapter/PoolTest.php @@ -14,6 +14,7 @@ use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Limit; use Utopia\Database\PDO; +use Utopia\Pools\Adapter\Stack; use Utopia\Pools\Pool as UtopiaPool; class PoolTest extends Base @@ -43,7 +44,7 @@ public function getDatabase(): Database $redis->flushAll(); $cache = new Cache(new RedisAdapter($redis)); - $pool = new UtopiaPool('mysql', 10, function () { + $pool = new UtopiaPool(new Stack(), 'mysql', 10, function () { $dbHost = 'mysql'; $dbPort = '3307'; $dbUser = 'root'; From ef186b63519bd1bda740e3eb95d531f06358d7be Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 29 Jan 2026 11:56:38 +0000 Subject: [PATCH 4/8] Add cache reconnection test --- tests/e2e/Adapter/Scopes/GeneralTests.php | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index bf4280163..fe74f9b8a 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -4,6 +4,8 @@ use Exception; use Throwable; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -697,6 +699,72 @@ public function testCacheFallback(): void $this->assertCount(1, $database->find('testRedisFallback', [Query::equal('string', ['text📝'])])); } + public function testCacheReconnect(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + if (!$database->getAdapter()->getSupportForCacheSkipOnFailure()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Create new cache with reconnection enabled + $redis = new \Redis(); + $redis->connect('redis', 6379); + $cache = new Cache((new RedisAdapter($redis))->setMaxRetries(2)); + $database->setCache($cache); + + $database->getAuthorization()->cleanRoles(); + $database->getAuthorization()->addRole(Role::any()->toString()); + + $database->createCollection('testCacheReconnect', attributes: [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => true, + ]) + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]); + + $database->createDocument('testCacheReconnect', new Document([ + '$id' => 'reconnect_doc', + 'title' => 'Test Document', + ])); + + // Cache the document + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); + $this->assertEquals('Test Document', $doc->getAttribute('title')); + + // Bring down Redis + $stdout = ''; + $stderr = ''; + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker stop', "", $stdout, $stderr); + sleep(1); + + // Bring back Redis + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); + sleep(3); + // Cache should reconnect - read should work + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); + $this->assertEquals('Test Document', $doc->getAttribute('title')); + // Update should work after reconnect + $database->updateDocument('testCacheReconnect', 'reconnect_doc', new Document([ + '$id' => 'reconnect_doc', + 'title' => 'Updated Title', + ])); + + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); + $this->assertEquals('Updated Title', $doc->getAttribute('title')); + + // Cleanup + $database->deleteCollection('testCacheReconnect'); + } } From 9f2da33fc838699c82f94a06d1cd3ba773db6c87 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 29 Jan 2026 12:14:52 +0000 Subject: [PATCH 5/8] Fix cache reconnect test timing --- tests/e2e/Adapter/Scopes/GeneralTests.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index fe74f9b8a..d69bf5c86 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -709,6 +709,9 @@ public function testCacheReconnect(): void return; } + // Wait for Redis to be fully healthy after previous test + sleep(3); + // Create new cache with reconnection enabled $redis = new \Redis(); $redis->connect('redis', 6379); @@ -749,7 +752,7 @@ public function testCacheReconnect(): void // Bring back Redis Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); - sleep(3); + sleep(5); // Cache should reconnect - read should work $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); From 0d9d1eb70dda342c26a6a8be679ce35c778c02bc Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 29 Jan 2026 12:27:53 +0000 Subject: [PATCH 6/8] Increase cache reconnect test wait times --- tests/e2e/Adapter/Scopes/GeneralTests.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index d69bf5c86..853255863 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -710,12 +710,13 @@ public function testCacheReconnect(): void } // Wait for Redis to be fully healthy after previous test - sleep(3); + sleep(5); - // Create new cache with reconnection enabled + // Create new cache with reconnection enabled and verify connection $redis = new \Redis(); $redis->connect('redis', 6379); - $cache = new Cache((new RedisAdapter($redis))->setMaxRetries(2)); + $redis->ping(); // Verify connection is healthy + $cache = new Cache((new RedisAdapter($redis))->setMaxRetries(3)); $database->setCache($cache); $database->getAuthorization()->cleanRoles(); @@ -752,7 +753,7 @@ public function testCacheReconnect(): void // Bring back Redis Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); - sleep(5); + sleep(7); // Cache should reconnect - read should work $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); From eb56c87944dc46007c2f9fa86779597f5804a612 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 29 Jan 2026 12:48:36 +0000 Subject: [PATCH 7/8] Fix cache reconnect test for Mirror adapter --- tests/e2e/Adapter/Scopes/GeneralTests.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index 853255863..27ea5f7ea 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -20,6 +20,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; +use Utopia\Database\Mirror; use Utopia\Database\Query; trait GeneralTests @@ -710,13 +711,23 @@ public function testCacheReconnect(): void } // Wait for Redis to be fully healthy after previous test - sleep(5); + sleep(3); - // Create new cache with reconnection enabled and verify connection + // Create new cache with reconnection enabled $redis = new \Redis(); $redis->connect('redis', 6379); - $redis->ping(); // Verify connection is healthy $cache = new Cache((new RedisAdapter($redis))->setMaxRetries(3)); + + // For Mirror, we need to set cache on both source and destination + if ($database instanceof Mirror) { + $database->getSource()->setCache($cache); + + $mirrorRedis = new \Redis(); + $mirrorRedis->connect('redis-mirror', 6379); + $mirrorCache = new Cache((new RedisAdapter($mirrorRedis))->setMaxRetries(3)); + $database->getDestination()->setCache($mirrorCache); + } + $database->setCache($cache); $database->getAuthorization()->cleanRoles(); @@ -753,7 +764,7 @@ public function testCacheReconnect(): void // Bring back Redis Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); - sleep(7); + sleep(3); // Cache should reconnect - read should work $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); From 67b3f5f56fad24977c78a900ef8778e9a5d801ae Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 29 Jan 2026 13:01:35 +0000 Subject: [PATCH 8/8] Improve cache reconnect test with try/finally and readiness probe --- tests/e2e/Adapter/Scopes/GeneralTests.php | 109 ++++++++++++++-------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index 27ea5f7ea..9ac5ccd35 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -711,7 +711,7 @@ public function testCacheReconnect(): void } // Wait for Redis to be fully healthy after previous test - sleep(3); + $this->waitForRedis(); // Create new cache with reconnection enabled $redis = new \Redis(); @@ -733,53 +733,80 @@ public function testCacheReconnect(): void $database->getAuthorization()->cleanRoles(); $database->getAuthorization()->addRole(Role::any()->toString()); - $database->createCollection('testCacheReconnect', attributes: [ - new Document([ - '$id' => ID::custom('title'), - 'type' => Database::VAR_STRING, - 'size' => 255, - 'required' => true, - ]) - ], permissions: [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()) - ]); + try { + $database->createCollection('testCacheReconnect', attributes: [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => true, + ]) + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]); - $database->createDocument('testCacheReconnect', new Document([ - '$id' => 'reconnect_doc', - 'title' => 'Test Document', - ])); + $database->createDocument('testCacheReconnect', new Document([ + '$id' => 'reconnect_doc', + 'title' => 'Test Document', + ])); - // Cache the document - $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); - $this->assertEquals('Test Document', $doc->getAttribute('title')); + // Cache the document + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); + $this->assertEquals('Test Document', $doc->getAttribute('title')); - // Bring down Redis - $stdout = ''; - $stderr = ''; - Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker stop', "", $stdout, $stderr); - sleep(1); + // Bring down Redis + $stdout = ''; + $stderr = ''; + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker stop', "", $stdout, $stderr); + sleep(1); - // Bring back Redis - Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); - sleep(3); + // Bring back Redis + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); + $this->waitForRedis(); - // Cache should reconnect - read should work - $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); - $this->assertEquals('Test Document', $doc->getAttribute('title')); + // Cache should reconnect - read should work + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); + $this->assertEquals('Test Document', $doc->getAttribute('title')); - // Update should work after reconnect - $database->updateDocument('testCacheReconnect', 'reconnect_doc', new Document([ - '$id' => 'reconnect_doc', - 'title' => 'Updated Title', - ])); + // Update should work after reconnect + $database->updateDocument('testCacheReconnect', 'reconnect_doc', new Document([ + '$id' => 'reconnect_doc', + 'title' => 'Updated Title', + ])); - $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); - $this->assertEquals('Updated Title', $doc->getAttribute('title')); + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); + $this->assertEquals('Updated Title', $doc->getAttribute('title')); + } finally { + // Ensure Redis is running + $stdout = ''; + $stderr = ''; + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); + $this->waitForRedis(); + + // Cleanup collection if it exists + if ($database->exists() && !$database->getCollection('testCacheReconnect')->isEmpty()) { + $database->deleteCollection('testCacheReconnect'); + } + } + } - // Cleanup - $database->deleteCollection('testCacheReconnect'); + /** + * Wait for Redis to be ready with a readiness probe + */ + private function waitForRedis(int $maxRetries = 10, int $delayMs = 500): void + { + for ($i = 0; $i < $maxRetries; $i++) { + try { + $redis = new \Redis(); + $redis->connect('redis', 6379); + $redis->ping(); + return; + } catch (\RedisException $e) { + usleep($delayMs * 1000); + } + } } }