Skip to content

Commit 0e7c43e

Browse files
committed
Fix cache clobber
1 parent adfdf20 commit 0e7c43e

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

src/Database/Database.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4271,11 +4271,14 @@ public function getDocument(string $collection, string $id, array $queries = [],
42714271
$selections
42724272
);
42734273

4274-
try {
4275-
$cached = $this->cache->load($documentKey, self::TTL, $hashKey);
4276-
} catch (Exception $e) {
4277-
Console::warning('Warning: Failed to get document from cache: ' . $e->getMessage());
4278-
$cached = null;
4274+
$cached = null;
4275+
if (!$forUpdate) {
4276+
try {
4277+
$cached = $this->cache->load($documentKey, self::TTL, $hashKey);
4278+
} catch (Exception $e) {
4279+
Console::warning('Warning: Failed to get document from cache: ' . $e->getMessage());
4280+
$cached = null;
4281+
}
42794282
}
42804283

42814284
if ($cached) {
@@ -4348,7 +4351,7 @@ public function getDocument(string $collection, string $id, array $queries = [],
43484351
);
43494352

43504353
// Don't save to cache if it's part of a relationship
4351-
if (empty($relationships)) {
4354+
if (!$forUpdate && empty($relationships)) {
43524355
try {
43534356
$this->cache->save($documentKey, $document->getArrayCopy(), $hashKey);
43544357
$this->cache->save($collectionKey, 'empty', $documentKey);

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7456,6 +7456,54 @@ public function testRegexInjection(): void
74567456
$database->deleteCollection($collectionName);
74577457
}
74587458

7459+
public function testUpdateDocumentUsesFreshForUpdateReadWhenCacheIsStale(): void
7460+
{
7461+
/** @var Database $database */
7462+
$database = $this->getDatabase();
7463+
7464+
$collectionId = 'for_update_cache';
7465+
$database->createCollection($collectionId);
7466+
7467+
if ($database->getAdapter()->getSupportForAttributes()) {
7468+
$this->assertEquals(true, $database->createAttribute($collectionId, 'a', Database::VAR_STRING, 255, false));
7469+
$this->assertEquals(true, $database->createAttribute($collectionId, 'b', Database::VAR_STRING, 255, false));
7470+
}
7471+
7472+
$database->createDocument($collectionId, new Document([
7473+
'$id' => 'doc1',
7474+
'$permissions' => [
7475+
Permission::read(Role::any()),
7476+
Permission::update(Role::any()),
7477+
],
7478+
'a' => 'A1',
7479+
'b' => 'B1',
7480+
]));
7481+
7482+
// Prime cache with initial values.
7483+
$cached = $database->getDocument($collectionId, 'doc1');
7484+
$this->assertEquals('B1', $cached->getAttribute('b'));
7485+
7486+
$collection = $database->getCollection($collectionId);
7487+
7488+
// Simulate an out-of-band write that bypasses cache invalidation.
7489+
$outOfBand = $database->getAdapter()->getDocument($collection, 'doc1');
7490+
$outOfBand->setAttribute('b', 'B2');
7491+
$database->getAdapter()->updateDocument($collection, 'doc1', $outOfBand, true);
7492+
7493+
// Partial update should not overwrite untouched fields with stale cached values.
7494+
$updated = $database->updateDocument($collectionId, 'doc1', new Document([
7495+
'a' => 'A2',
7496+
]));
7497+
7498+
$this->assertEquals('A2', $updated->getAttribute('a'));
7499+
$this->assertEquals('B2', $updated->getAttribute('b'));
7500+
7501+
$fresh = $database->getDocument($collectionId, 'doc1');
7502+
$this->assertEquals('B2', $fresh->getAttribute('b'));
7503+
7504+
$database->deleteCollection($collectionId);
7505+
}
7506+
74597507
/**
74607508
* Test ReDoS (Regular Expression Denial of Service) with timeout protection
74617509
* This test verifies that ReDoS patterns either timeout properly or complete quickly,

0 commit comments

Comments
 (0)