Skip to content

Commit bc5b127

Browse files
authored
Merge pull request #494 from utopia-php/0.53.x-cache-fallback
Add cache fallback into DB library
2 parents 89e850b + fc8964e commit bc5b127

13 files changed

Lines changed: 140 additions & 23 deletions

File tree

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ RUN \
3434
git \
3535
brotli-dev \
3636
linux-headers \
37+
docker-cli \
38+
docker-cli-compose \
3739
&& docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \
3840
&& apk del postgresql-dev \
3941
&& rm -rf /var/cache/apk/*

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ services:
1515
- ./dev:/usr/src/code/dev
1616
- ./phpunit.xml:/usr/src/code/phpunit.xml
1717
- ./dev/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
18+
- /var/run/docker.sock:/var/run/docker.sock
19+
- ./docker-compose.yml:/usr/src/code/docker-compose.yml
1820

1921
adminer:
2022
image: adminer

src/Database/Adapter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,13 @@ abstract public function getSupportForCastIndexArray(): bool;
900900
*/
901901
abstract public function getSupportForUpserts(): bool;
902902

903+
/**
904+
* Is Cache Fallback supported?
905+
*
906+
* @return bool
907+
*/
908+
abstract public function getSupportForCacheSkipOnFailure(): bool;
909+
903910
/**
904911
* Get current attribute count from collection document
905912
*

src/Database/Adapter/Mongo.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,16 @@ public function getSupportForGetConnectionId(): bool
18001800
return false;
18011801
}
18021802

1803+
/**
1804+
* Is cache fallback supported?
1805+
*
1806+
* @return bool
1807+
*/
1808+
public function getSupportForCacheSkipOnFailure(): bool
1809+
{
1810+
return false;
1811+
}
1812+
18031813
/**
18041814
* Is get schema attributes supported?
18051815
*

src/Database/Adapter/SQL.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,16 @@ public function getSupportForGetConnectionId(): bool
402402
return true;
403403
}
404404

405+
/**
406+
* Is cache fallback supported?
407+
*
408+
* @return bool
409+
*/
410+
public function getSupportForCacheSkipOnFailure(): bool
411+
{
412+
return true;
413+
}
414+
405415
/**
406416
* Get current attribute count from collection document
407417
*

src/Database/Adapter/SQLite.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,15 +220,7 @@ public function createCollection(string $name, array $attributes = [], array $in
220220
$this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []);
221221

222222
} catch (PDOException $e) {
223-
$e = $this->processException($e);
224-
225-
if (!($e instanceof Duplicate)) {
226-
$this->getPDO()
227-
->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};")
228-
->execute();
229-
}
230-
231-
throw $e;
223+
throw $this->processException($e);
232224
}
233225
return true;
234226
}

src/Database/Database.php

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Exception;
66
use Utopia\Cache\Cache;
7+
use Utopia\CLI\Console;
78
use Utopia\Database\Exception as DatabaseException;
89
use Utopia\Database\Exception\Authorization as AuthorizationException;
910
use Utopia\Database\Exception\Conflict as ConflictException;
@@ -441,14 +442,20 @@ function (?string $value) {
441442

442443
/**
443444
* Add listener to events
445+
* Passing a null $callback will remove the listener
444446
*
445447
* @param string $event
446448
* @param string $name
447-
* @param callable $callback
449+
* @param ?callable $callback
448450
* @return static
449451
*/
450-
public function on(string $event, string $name, callable $callback): static
452+
public function on(string $event, string $name, ?callable $callback): static
451453
{
454+
if (empty($callback)) {
455+
unset($this->listeners[$event][$name]);
456+
return $this;
457+
}
458+
452459
if (!isset($this->listeners[$event])) {
453460
$this->listeners[$event] = [];
454461
}
@@ -2992,8 +2999,15 @@ public function getDocument(string $collection, string $id, array $queries = [],
29922999
$documentCacheHash .= ':' . \md5(\implode($selections));
29933000
}
29943001

2995-
if ($cache = $this->cache->load($documentCacheKey, self::TTL, $documentCacheHash)) {
2996-
$document = new Document($cache);
3002+
try {
3003+
$cached = $this->cache->load($documentCacheKey, self::TTL, $documentCacheHash);
3004+
} catch (Exception $e) {
3005+
Console::warning('Warning: Failed to get document from cache: ' . $e->getMessage());
3006+
$cached = null;
3007+
}
3008+
3009+
if ($cached) {
3010+
$document = new Document($cached);
29973011

29983012
if ($collection->getId() !== self::METADATA) {
29993013
if (!$validator->isValid([
@@ -3042,8 +3056,12 @@ public function getDocument(string $collection, string $id, array $queries = [],
30423056

30433057
// Don't save to cache if it's part of a relationship
30443058
if (empty($relationships)) {
3045-
$this->cache->save($documentCacheKey, $document->getArrayCopy(), $documentCacheHash);
3046-
$this->cache->save($collectionCacheKey, 'empty', $documentCacheKey);
3059+
try {
3060+
$this->cache->save($documentCacheKey, $document->getArrayCopy(), $documentCacheHash);
3061+
$this->cache->save($collectionCacheKey, 'empty', $documentCacheKey);
3062+
} catch (Exception $e) {
3063+
Console::warning('Failed to save document to cache: ' . $e->getMessage());
3064+
}
30473065
}
30483066

30493067
// Remove internal attributes if not queried for select query
@@ -3962,6 +3980,7 @@ public function updateDocument(string $collection, string $id, Document $documen
39623980
}
39633981

39643982
$this->adapter->updateDocument($collection->getId(), $id, $document);
3983+
$this->purgeCachedDocument($collection->getId(), $id);
39653984

39663985
return $document;
39673986
});
@@ -3972,7 +3991,6 @@ public function updateDocument(string $collection, string $id, Document $documen
39723991

39733992
$document = $this->decode($collection, $document);
39743993

3975-
$this->purgeCachedDocument($collection->getId(), $id);
39763994
$this->trigger(self::EVENT_DOCUMENT_UPDATE, $document);
39773995

39783996
return $document;
@@ -4888,10 +4906,12 @@ public function deleteDocument(string $collection, string $id): bool
48884906
$document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document));
48894907
}
48904908

4891-
return $this->adapter->deleteDocument($collection->getId(), $id);
4892-
});
4909+
$result = $this->adapter->deleteDocument($collection->getId(), $id);
48934910

4894-
$this->purgeCachedDocument($collection->getId(), $id);
4911+
$this->purgeCachedDocument($collection->getId(), $id);
4912+
4913+
return $result;
4914+
});
48954915

48964916
$this->trigger(self::EVENT_DOCUMENT_DELETE, $document);
48974917

@@ -5424,6 +5444,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba
54245444
public function purgeCachedCollection(string $collectionId): bool
54255445
{
54265446
$collectionKey = $this->cacheName . '-cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':collection:' . $collectionId;
5447+
54275448
$documentKeys = $this->cache->list($collectionKey);
54285449
foreach ($documentKeys as $documentKey) {
54295450
$this->cache->purge($documentKey);

src/Database/Mirror.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public function disableValidation(): static
157157
return $this;
158158
}
159159

160-
public function on(string $event, string $name, callable $callback): static
160+
public function on(string $event, string $name, ?callable $callback): static
161161
{
162162
$this->source->on($event, $name, $callback);
163163

tests/e2e/Adapter/Base.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Exception;
66
use PHPUnit\Framework\TestCase;
77
use Throwable;
8+
use Utopia\CLI\Console;
89
use Utopia\Database\Adapter\SQL;
910
use Utopia\Database\Database;
1011
use Utopia\Database\DateTime;
@@ -17729,6 +17730,77 @@ public function testEvents(): void
1772917730
$database->deleteAttribute($collectionId, 'attr1');
1773017731
$database->deleteCollection($collectionId);
1773117732
$database->delete('hellodb');
17733+
17734+
// Remove all listeners
17735+
$database->on(Database::EVENT_ALL, 'test', null);
17736+
$database->on(Database::EVENT_ALL, 'should-not-execute', null);
1773217737
});
1773317738
}
17739+
17740+
public function testCacheFallback(): void
17741+
{
17742+
if (!static::getDatabase()->getAdapter()->getSupportForCacheSkipOnFailure()) {
17743+
$this->expectNotToPerformAssertions();
17744+
return;
17745+
}
17746+
17747+
Authorization::cleanRoles();
17748+
Authorization::setRole(Role::any()->toString());
17749+
$database = static::getDatabase();
17750+
17751+
// Write mock data
17752+
$database->createCollection('testRedisFallback', attributes: [
17753+
new Document([
17754+
'$id' => ID::custom('string'),
17755+
'type' => Database::VAR_STRING,
17756+
'size' => 767,
17757+
'required' => true,
17758+
])
17759+
], permissions: [
17760+
Permission::read(Role::any()),
17761+
Permission::create(Role::any()),
17762+
Permission::update(Role::any()),
17763+
Permission::delete(Role::any())
17764+
]);
17765+
17766+
$database->createDocument('testRedisFallback', new Document([
17767+
'$id' => 'doc1',
17768+
'string' => 'text📝',
17769+
]));
17770+
17771+
$database->createIndex('testRedisFallback', 'index1', Database::INDEX_KEY, ['string']);
17772+
$this->assertCount(1, $database->find('testRedisFallback', [Query::equal('string', ['text📝'])]));
17773+
17774+
// Bring down Redis
17775+
$stdout = '';
17776+
$stderr = '';
17777+
Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker stop', "", $stdout, $stderr);
17778+
17779+
// Check we can read data still
17780+
$this->assertCount(1, $database->find('testRedisFallback', [Query::equal('string', ['text📝'])]));
17781+
$this->assertFalse(($database->getDocument('testRedisFallback', 'doc1'))->isEmpty());
17782+
17783+
// Check we cannot modify data
17784+
try {
17785+
$database->updateDocument('testRedisFallback', 'doc1', new Document([
17786+
'string' => 'text📝 updated',
17787+
]));
17788+
$this->fail('Failed to throw exception');
17789+
} catch (\Throwable $e) {
17790+
$this->assertEquals('Redis server redis:6379 went away', $e->getMessage());
17791+
}
17792+
17793+
try {
17794+
$database->deleteDocument('testRedisFallback', 'doc1');
17795+
$this->fail('Failed to throw exception');
17796+
} catch (\Throwable $e) {
17797+
$this->assertEquals('Redis server redis:6379 went away', $e->getMessage());
17798+
}
17799+
17800+
// Bring backup Redis
17801+
Console::execute('docker ps -a --filter "name=utopia-redis" --format "{{.Names}}" | xargs -r docker start', "", $stdout, $stderr);
17802+
sleep(5);
17803+
17804+
$this->assertCount(1, $database->find('testRedisFallback', [Query::equal('string', ['text📝'])]));
17805+
}
1773417806
}

tests/e2e/Adapter/MariaDBTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static function getDatabase(bool $fresh = false): Database
4141
$dbPass = 'password';
4242

4343
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes());
44+
4445
$redis = new Redis();
4546
$redis->connect('redis', 6379);
4647
$redis->flushAll();

0 commit comments

Comments
 (0)