|
4 | 4 |
|
5 | 5 | use Exception; |
6 | 6 | use Throwable; |
| 7 | +use Utopia\Cache\Adapter\Redis as RedisAdapter; |
| 8 | +use Utopia\Cache\Cache; |
7 | 9 | use Utopia\CLI\Console; |
8 | 10 | use Utopia\Database\Database; |
9 | 11 | use Utopia\Database\Document; |
|
18 | 20 | use Utopia\Database\Helpers\ID; |
19 | 21 | use Utopia\Database\Helpers\Permission; |
20 | 22 | use Utopia\Database\Helpers\Role; |
| 23 | +use Utopia\Database\Mirror; |
21 | 24 | use Utopia\Database\Query; |
22 | 25 |
|
23 | 26 | trait GeneralTests |
@@ -697,6 +700,113 @@ public function testCacheFallback(): void |
697 | 700 | $this->assertCount(1, $database->find('testRedisFallback', [Query::equal('string', ['text📝'])])); |
698 | 701 | } |
699 | 702 |
|
| 703 | + public function testCacheReconnect(): void |
| 704 | + { |
| 705 | + /** @var Database $database */ |
| 706 | + $database = $this->getDatabase(); |
| 707 | + |
| 708 | + if (!$database->getAdapter()->getSupportForCacheSkipOnFailure()) { |
| 709 | + $this->expectNotToPerformAssertions(); |
| 710 | + return; |
| 711 | + } |
| 712 | + |
| 713 | + // Wait for Redis to be fully healthy after previous test |
| 714 | + $this->waitForRedis(); |
700 | 715 |
|
| 716 | + // Create new cache with reconnection enabled |
| 717 | + $redis = new \Redis(); |
| 718 | + $redis->connect('redis', 6379); |
| 719 | + $cache = new Cache((new RedisAdapter($redis))->setMaxRetries(3)); |
701 | 720 |
|
| 721 | + // For Mirror, we need to set cache on both source and destination |
| 722 | + if ($database instanceof Mirror) { |
| 723 | + $database->getSource()->setCache($cache); |
| 724 | + |
| 725 | + $mirrorRedis = new \Redis(); |
| 726 | + $mirrorRedis->connect('redis-mirror', 6379); |
| 727 | + $mirrorCache = new Cache((new RedisAdapter($mirrorRedis))->setMaxRetries(3)); |
| 728 | + $database->getDestination()->setCache($mirrorCache); |
| 729 | + } |
| 730 | + |
| 731 | + $database->setCache($cache); |
| 732 | + |
| 733 | + $database->getAuthorization()->cleanRoles(); |
| 734 | + $database->getAuthorization()->addRole(Role::any()->toString()); |
| 735 | + |
| 736 | + try { |
| 737 | + $database->createCollection('testCacheReconnect', attributes: [ |
| 738 | + new Document([ |
| 739 | + '$id' => ID::custom('title'), |
| 740 | + 'type' => Database::VAR_STRING, |
| 741 | + 'size' => 255, |
| 742 | + 'required' => true, |
| 743 | + ]) |
| 744 | + ], permissions: [ |
| 745 | + Permission::read(Role::any()), |
| 746 | + Permission::create(Role::any()), |
| 747 | + Permission::update(Role::any()), |
| 748 | + Permission::delete(Role::any()) |
| 749 | + ]); |
| 750 | + |
| 751 | + $database->createDocument('testCacheReconnect', new Document([ |
| 752 | + '$id' => 'reconnect_doc', |
| 753 | + 'title' => 'Test Document', |
| 754 | + ])); |
| 755 | + |
| 756 | + // Cache the document |
| 757 | + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); |
| 758 | + $this->assertEquals('Test Document', $doc->getAttribute('title')); |
| 759 | + |
| 760 | + // Bring down Redis |
| 761 | + $stdout = ''; |
| 762 | + $stderr = ''; |
| 763 | + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker stop', "", $stdout, $stderr); |
| 764 | + sleep(1); |
| 765 | + |
| 766 | + // Bring back Redis |
| 767 | + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); |
| 768 | + $this->waitForRedis(); |
| 769 | + |
| 770 | + // Cache should reconnect - read should work |
| 771 | + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); |
| 772 | + $this->assertEquals('Test Document', $doc->getAttribute('title')); |
| 773 | + |
| 774 | + // Update should work after reconnect |
| 775 | + $database->updateDocument('testCacheReconnect', 'reconnect_doc', new Document([ |
| 776 | + '$id' => 'reconnect_doc', |
| 777 | + 'title' => 'Updated Title', |
| 778 | + ])); |
| 779 | + |
| 780 | + $doc = $database->getDocument('testCacheReconnect', 'reconnect_doc'); |
| 781 | + $this->assertEquals('Updated Title', $doc->getAttribute('title')); |
| 782 | + } finally { |
| 783 | + // Ensure Redis is running |
| 784 | + $stdout = ''; |
| 785 | + $stderr = ''; |
| 786 | + Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr); |
| 787 | + $this->waitForRedis(); |
| 788 | + |
| 789 | + // Cleanup collection if it exists |
| 790 | + if ($database->exists() && !$database->getCollection('testCacheReconnect')->isEmpty()) { |
| 791 | + $database->deleteCollection('testCacheReconnect'); |
| 792 | + } |
| 793 | + } |
| 794 | + } |
| 795 | + |
| 796 | + /** |
| 797 | + * Wait for Redis to be ready with a readiness probe |
| 798 | + */ |
| 799 | + private function waitForRedis(int $maxRetries = 10, int $delayMs = 500): void |
| 800 | + { |
| 801 | + for ($i = 0; $i < $maxRetries; $i++) { |
| 802 | + try { |
| 803 | + $redis = new \Redis(); |
| 804 | + $redis->connect('redis', 6379); |
| 805 | + $redis->ping(); |
| 806 | + return; |
| 807 | + } catch (\RedisException $e) { |
| 808 | + usleep($delayMs * 1000); |
| 809 | + } |
| 810 | + } |
| 811 | + } |
702 | 812 | } |
0 commit comments