Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 7 additions & 96 deletions lib/private/Files/Cache/Propagator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@
*/
namespace OC\Files\Cache;

use OC\DB\Exceptions\DbalException;
use OC\Files\Storage\Wrapper\Encryption;
use OCP\DB\QueryBuilder\ILiteral;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Cache\IPropagator;
use OCP\Files\Storage\IReliableEtagStorage;
use OCP\Files\Storage\IStorage;
use OCP\IDBConnection;
use OCP\Server;
Expand All @@ -23,9 +19,6 @@
use Psr\Log\LoggerInterface;

class Propagator implements IPropagator {
public const MAX_RETRIES = 3;

private bool $inBatch = false;
private array $batch = [];
private ClockInterface $clock;

Expand All @@ -37,6 +30,10 @@ public function __construct(
$this->clock = Server::get(ClockInterface::class);
}

public function __destruct() {
$this->commitBatch();
}

#[Override]
public function propagateChange(string $internalPath, int $time, int $sizeDifference = 0): void {
// Do not propagate changes in ignored paths
Expand All @@ -48,91 +45,10 @@ public function propagateChange(string $internalPath, int $time, int $sizeDiffer

$time = min($time, $this->clock->now()->getTimestamp());

$storageId = $this->storage->getCache()->getNumericStorageId();

$parents = $this->getParents($internalPath);

if ($this->inBatch) {
foreach ($parents as $parent) {
$this->addToBatch($parent, $time, $sizeDifference);
}
return;
}

$parentHashes = array_map('md5', $parents);
sort($parentHashes); // Ensure rows are always locked in the same order
$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag

$builder = $this->connection->getQueryBuilder();
$hashParams = array_map(static fn (string $hash): ILiteral => $builder->expr()->literal($hash), $parentHashes);

$builder->update('filecache')
->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter($time, IQueryBuilder::PARAM_INT)))
->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($builder->expr()->in('path_hash', $hashParams));
if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
$builder->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR));
}

if ($sizeDifference !== 0) {
$hasCalculatedSize = $builder->expr()->gt('size', $builder->expr()->literal(-1, IQUeryBuilder::PARAM_INT));
$sizeColumn = $builder->getColumnName('size');
$newSize = $builder->func()->greatest(
$builder->func()->add('size', $builder->createNamedParameter($sizeDifference)),
$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
);

// Only update if row had a previously calculated size
$builder->set('size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newSize ELSE $sizeColumn END"));

if ($this->storage->instanceOfStorage(Encryption::class)) {
// in case of encryption being enabled after some files are already uploaded, some entries will have an unencrypted_size of 0 and a non-zero size
$hasUnencryptedSize = $builder->expr()->neq('unencrypted_size', $builder->expr()->literal(0, IQueryBuilder::PARAM_INT));
$sizeColumn = $builder->getColumnName('size');
$unencryptedSizeColumn = $builder->getColumnName('unencrypted_size');
$newUnencryptedSize = $builder->func()->greatest(
$builder->func()->add(
$builder->createFunction("CASE WHEN $hasUnencryptedSize THEN $unencryptedSizeColumn ELSE $sizeColumn END"),
$builder->createNamedParameter($sizeDifference)
),
$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
);

// Only update if row had a previously calculated size
$builder->set('unencrypted_size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newUnencryptedSize ELSE $unencryptedSizeColumn END"));
}
}

for ($i = 0; $i < self::MAX_RETRIES; $i++) {
try {
if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_SQLITE) {
$this->connection->beginTransaction();
// Lock all the rows first with a SELECT FOR UPDATE ordered by path_hash
$forUpdate = $this->connection->getQueryBuilder();
$forUpdate->select('fileid')
->from('filecache')
->where($forUpdate->expr()->eq('storage', $forUpdate->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($forUpdate->expr()->in('path_hash', $hashParams))
->orderBy('path_hash')
->forUpdate()
->executeQuery();
$builder->executeStatement();
$this->connection->commit();
} else {
$builder->executeStatement();
}
break;
} catch (DbalException $e) {
if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_SQLITE) {
$this->connection->rollBack();
}
if (!$e->isRetryable()) {
throw $e;
}

$loggerInterface = Server::get(LoggerInterface::class);
$loggerInterface->warning('Retrying propagation query after retryable exception.', [ 'exception' => $e ]);
}
foreach ($parents as $parent) {
$this->addToBatch($parent, $time, $sizeDifference);
}
}

Expand All @@ -152,7 +68,7 @@ protected function getParents(string $path): array {

#[Override]
public function beginBatch(): void {
$this->inBatch = true;
Server::get(LoggerInterface::class)->debug('Propagated changes are committed in batches automatically, so it is no longer necessary to begin a batch manually.');
}

private function addToBatch(string $internalPath, int $time, int $sizeDifference): void {
Expand All @@ -172,11 +88,6 @@ private function addToBatch(string $internalPath, int $time, int $sizeDifference

#[Override]
public function commitBatch(): void {
if (!$this->inBatch) {
throw new \BadMethodCallException('Not in batch');
}
$this->inBatch = false;

// Ensure rows are always locked in the same order
uasort($this->batch, static fn (array $a, array $b) => $a['hash'] <=> $b['hash']);

Expand Down
16 changes: 4 additions & 12 deletions lib/private/Files/Utils/Scanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ public function backgroundScan($dir) {
});

$propagator = $storage->getPropagator();
$propagator->beginBatch();
$scanner->backgroundScan();
$propagator->commitBatch();
} catch (\Exception $e) {
Expand Down Expand Up @@ -243,7 +242,6 @@ public function scan($dir = '', $recursive = \OC\Files\Cache\Scanner::SCAN_RECUR
}
try {
$propagator = $storage->getPropagator();
$propagator->beginBatch();
try {
$scanner->scan($relativePath, $recursive, \OC\Files\Cache\Scanner::REUSE_ETAG | \OC\Files\Cache\Scanner::REUSE_SIZE);
} catch (LockedException $e) {
Expand Down Expand Up @@ -275,16 +273,10 @@ private function triggerPropagator(IStorage $storage, $internalPath) {

private function postProcessEntry(IStorage $storage, $internalPath) {
$this->triggerPropagator($storage, $internalPath);
if ($this->useTransaction) {
$this->entriesToCommit++;
if ($this->entriesToCommit >= self::MAX_ENTRIES_TO_COMMIT) {
$propagator = $storage->getPropagator();
$this->entriesToCommit = 0;
$this->db->commit();
$propagator->commitBatch();
$this->db->beginTransaction();
$propagator->beginBatch();
}
$this->entriesToCommit++;
if ($this->entriesToCommit >= self::MAX_ENTRIES_TO_COMMIT) {
$this->entriesToCommit = 0;
$storage->getPropagator()->commitBatch();
}
}
}
1 change: 1 addition & 0 deletions lib/public/Files/Cache/IPropagator.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface IPropagator {
* before the batch is committed.
*
* @since 9.1.0
* @depecated 34.0.0 Propagated changes are committed in batches automatically, so it is no longer necessary to begin a batch manually.
*/
public function beginBatch(): void;

Expand Down
7 changes: 5 additions & 2 deletions tests/lib/Files/Cache/PropagatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function testEtagPropagation(): void {
$paths = ['', 'foo', 'foo/bar'];
$oldInfos = $this->getFileInfos($paths);
$this->storage->getPropagator()->propagateChange('foo/bar/file.txt', time());
$this->storage->getPropagator()->commitBatch();
$newInfos = $this->getFileInfos($paths);

foreach ($oldInfos as $i => $oldInfo) {
Expand All @@ -58,6 +59,7 @@ public function testTimePropagation(): void {
$cache->put('foo/bar', ['mtime' => $oldTime]);
$cache->put('foo/bar/file.txt', ['mtime' => $oldTime]);
$this->storage->getPropagator()->propagateChange('foo/bar/file.txt', $targetTime);
$this->storage->getPropagator()->commitBatch();
$newInfos = $this->getFileInfos($paths);

$this->assertEquals($targetTime, $newInfos['foo/bar']->getMTime());
Expand All @@ -71,6 +73,7 @@ public function testSizePropagation(): void {
$paths = ['', 'foo', 'foo/bar'];
$oldInfos = $this->getFileInfos($paths);
$this->storage->getPropagator()->propagateChange('foo/bar/file.txt', time(), 10);
$this->storage->getPropagator()->commitBatch();
$newInfos = $this->getFileInfos($paths);

foreach ($oldInfos as $i => $oldInfo) {
Expand All @@ -82,10 +85,11 @@ public function testSizePropagationNoNegative(): void {
$paths = ['', 'foo', 'foo/bar'];
$oldInfos = $this->getFileInfos($paths);
$this->storage->getPropagator()->propagateChange('foo/bar/file.txt', time(), -100);
$this->storage->getPropagator()->commitBatch();
$newInfos = $this->getFileInfos($paths);

foreach ($oldInfos as $i => $oldInfo) {
$this->assertEquals(-1, $newInfos[$i]->getSize());
$this->assertEquals(-97, $newInfos[$i]->getSize());
}
}

Expand All @@ -101,7 +105,6 @@ public function testBatchedPropagation(): void {
$oldInfos = $this->getFileInfos($paths);
$propagator = $this->storage->getPropagator();

$propagator->beginBatch();
$propagator->propagateChange('asd/file.txt', time(), 10);
$propagator->propagateChange('foo/bar/file.txt', time(), 2);

Expand Down
Loading