feat(filecache): Scale DB query created when deleting file from filecache

Instead of creating a CacheEntryRemovedEvent for each deleted files,
create a single CacheEntriesRemovedEvent which wrap multiple
CacheEntryRemovedEvent.

This allow listener to optimize the query they do when multiple files
are deleted at the same time (e.g. when deleting a folder).

Signed-off-by: Carl Schwan <carl.schwan@nextclound.com>
pull/54277/head
Carl Schwan 10 months ago committed by Carl Schwan
parent ebfdbf86b9
commit fd3878448b
No known key found for this signature in database
GPG Key ID: 02325448204E452A
  1. 4
      apps/files/lib/AppInfo/Application.php
  2. 28
      apps/files/lib/Listener/SyncLivePhotosListener.php
  3. 18
      apps/files/lib/Service/LivePhotosService.php
  4. 1
      lib/composer/composer/autoload_classmap.php
  5. 1
      lib/composer/composer/autoload_static.php
  6. 11
      lib/private/Files/Cache/Cache.php
  7. 18
      lib/private/FilesMetadata/FilesMetadataManager.php
  8. 22
      lib/private/FilesMetadata/Listener/MetadataDelete.php
  9. 26
      lib/private/FilesMetadata/Service/IndexRequestService.php
  10. 22
      lib/private/FilesMetadata/Service/MetadataRequestService.php
  11. 35
      lib/public/Files/Cache/CacheEntriesRemovedEvent.php
  12. 4
      lib/public/Files/Cache/CacheEntryRemovedEvent.php
  13. 11
      lib/public/FilesMetadata/IFilesMetadataManager.php

@ -37,7 +37,7 @@ use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Cache\CacheEntriesRemovedEvent;
use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
@ -115,7 +115,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
$context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(BeforeNodeDeletedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class, 1); // Ensure this happen before the metadata are deleted.
$context->registerEventListener(CacheEntriesRemovedEvent::class, SyncLivePhotosListener::class, 1); // Ensure this happen before the metadata are deleted.
$context->registerEventListener(BeforeNodeCopiedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(NodeCopiedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class);

@ -17,7 +17,7 @@ use OCA\Files\Service\LivePhotosService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Exceptions\AbortedEventException;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Cache\CacheEntriesRemovedEvent;
use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
@ -63,8 +63,8 @@ class SyncLivePhotosListener implements IEventListener {
$peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getSource()->getId());
} elseif ($event instanceof BeforeNodeDeletedEvent) {
$peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getNode()->getId());
} elseif ($event instanceof CacheEntryRemovedEvent) {
$peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getFileId());
} elseif ($event instanceof CacheEntriesRemovedEvent) {
$this->handleCacheEntriesRemovedEvent($event);
}
if ($peerFileId === null) {
@ -83,12 +83,30 @@ class SyncLivePhotosListener implements IEventListener {
$this->handleMove($event->getSource(), $event->getTarget(), $peerFile);
} elseif ($event instanceof BeforeNodeDeletedEvent) {
$this->handleDeletion($event, $peerFile);
} elseif ($event instanceof CacheEntryRemovedEvent) {
$peerFile->delete();
}
}
}
public function handleCacheEntriesRemovedEvent(CacheEntriesRemovedEvent $cacheEntriesRemovedEvent): void {
$entries = $cacheEntriesRemovedEvent->getCacheEntryRemovedEvents();
$fileIds = [];
foreach ($entries as $entry) {
$fileIds[] = $entry->getFileId();
}
$peerFileIds = $this->livePhotosService->getLivePhotoPeerIds($fileIds);
foreach ($peerFileIds as $peerFileId) {
// Check the user's folder.
$peerFile = $this->userFolder->getFirstNodeById($peerFileId);
if ($peerFile === null) {
return; // Peer file not found.
}
$peerFile->delete();
}
}
private function runMoveOrCopyChecks(Node $sourceFile, Node $targetFile, Node $peerFile): void {
$targetParent = $targetFile->getParent();
$sourceExtension = $sourceFile->getExtension();

@ -33,4 +33,22 @@ class LivePhotosService {
return (int)$metadata->getString('files-live-photo');
}
/**
* Get the associated live photo for multiple file ids
* @param int[] $fileIds
* @return int[]
*/
public function getLivePhotoPeerIds(array $fileIds): array {
$metadata = $this->filesMetadataManager->getMetadataForFiles($fileIds);
$peersIds = [];
foreach ($metadata as $item) {
if (!$item->hasKey('files-live-photo')) {
continue;
}
$peersIds[] = (int)$item->getString('files-live-photo');
}
return $peersIds;
}
}

@ -403,6 +403,7 @@ return array(
'OCP\\Files\\AlreadyExistsException' => $baseDir . '/lib/public/Files/AlreadyExistsException.php',
'OCP\\Files\\AppData\\IAppDataFactory' => $baseDir . '/lib/public/Files/AppData/IAppDataFactory.php',
'OCP\\Files\\Cache\\AbstractCacheEvent' => $baseDir . '/lib/public/Files/Cache/AbstractCacheEvent.php',
'OCP\\Files\\Cache\\CacheEntriesRemovedEvent' => $baseDir . '/lib/public/Files/Cache/CacheEntriesRemovedEvent.php',
'OCP\\Files\\Cache\\CacheEntryInsertedEvent' => $baseDir . '/lib/public/Files/Cache/CacheEntryInsertedEvent.php',
'OCP\\Files\\Cache\\CacheEntryRemovedEvent' => $baseDir . '/lib/public/Files/Cache/CacheEntryRemovedEvent.php',
'OCP\\Files\\Cache\\CacheEntryUpdatedEvent' => $baseDir . '/lib/public/Files/Cache/CacheEntryUpdatedEvent.php',

@ -444,6 +444,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Files\\AlreadyExistsException' => __DIR__ . '/../../..' . '/lib/public/Files/AlreadyExistsException.php',
'OCP\\Files\\AppData\\IAppDataFactory' => __DIR__ . '/../../..' . '/lib/public/Files/AppData/IAppDataFactory.php',
'OCP\\Files\\Cache\\AbstractCacheEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/AbstractCacheEvent.php',
'OCP\\Files\\Cache\\CacheEntriesRemovedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/CacheEntriesRemovedEvent.php',
'OCP\\Files\\Cache\\CacheEntryInsertedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/CacheEntryInsertedEvent.php',
'OCP\\Files\\Cache\\CacheEntryRemovedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/CacheEntryRemovedEvent.php',
'OCP\\Files\\Cache\\CacheEntryUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/CacheEntryUpdatedEvent.php',

@ -18,6 +18,7 @@ use OC\SystemConfig;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\CacheEntriesRemovedEvent;
use OCP\Files\Cache\CacheEntryInsertedEvent;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Cache\CacheEntryUpdatedEvent;
@ -633,6 +634,7 @@ class Cache implements ICache {
$query->executeStatement();
}
$cacheEntryRemovedEvents = [];
foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
$cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
$this->storage,
@ -640,8 +642,11 @@ class Cache implements ICache {
$fileId,
$this->getNumericStorageId()
);
$cacheEntryRemovedEvents[] = $cacheEntryRemovedEvent;
$this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
}
$this->eventDispatcher->dispatchTyped(new CacheEntriesRemovedEvent($cacheEntryRemovedEvents));
}
/**
@ -805,7 +810,11 @@ class Cache implements ICache {
if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
\OCP\Server::get(\OCP\Files\Config\IUserMountCache::class)->clear();
$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
$event = new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId());
$this->eventDispatcher->dispatchTyped($event);
$this->eventDispatcher->dispatchTyped(new CacheEntriesRemovedEvent([$event]));
$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
$this->eventDispatcher->dispatchTyped($event);

@ -20,7 +20,7 @@ use OCP\DB\Exception;
use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Cache\CacheEntriesRemovedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\InvalidPathException;
use OCP\Files\Node;
@ -214,6 +214,20 @@ class FilesMetadataManager implements IFilesMetadataManager {
}
}
public function deleteMetadataForFiles(array $fileIds): void {
try {
$this->metadataRequestService->dropMetadataForFiles($fileIds);
} catch (Exception $e) {
$this->logger->warning('issue while deleteMetadata', ['exception' => $e, 'fileIds' => $fileIds]);
}
try {
$this->indexRequestService->dropIndexForFiles($fileIds);
} catch (Exception $e) {
$this->logger->warning('issue while deleteMetadata', ['exception' => $e, 'fileIds' => $fileIds]);
}
}
/**
* @param IQueryBuilder $qb
* @param string $fileTableAlias alias of the table that contains data about files
@ -301,6 +315,6 @@ class FilesMetadataManager implements IFilesMetadataManager {
*/
public static function loadListeners(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addServiceListener(NodeWrittenEvent::class, MetadataUpdate::class);
$eventDispatcher->addServiceListener(CacheEntryRemovedEvent::class, MetadataDelete::class);
$eventDispatcher->addServiceListener(CacheEntriesRemovedEvent::class, MetadataDelete::class);
}
}

@ -11,14 +11,14 @@ namespace OC\FilesMetadata\Listener;
use Exception;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Cache\CacheEntriesRemovedEvent;
use OCP\FilesMetadata\IFilesMetadataManager;
use Psr\Log\LoggerInterface;
/**
* Handle file deletion event and remove stored metadata related to the deleted file
*
* @template-implements IEventListener<CacheEntryRemovedEvent>
* @template-implements IEventListener<CacheEntriesRemovedEvent>
*/
class MetadataDelete implements IEventListener {
public function __construct(
@ -28,15 +28,23 @@ class MetadataDelete implements IEventListener {
}
public function handle(Event $event): void {
if (!($event instanceof CacheEntryRemovedEvent)) {
if (!($event instanceof CacheEntriesRemovedEvent)) {
return;
}
try {
$nodeId = $event->getFileId();
if ($nodeId > 0) {
$this->filesMetadataManager->deleteMetadata($nodeId);
$entries = $event->getCacheEntryRemovedEvents();
$fileIds = [];
foreach ($entries as $entry) {
try {
$fileIds[] = $entry->getFileId();
} catch (Exception $e) {
$this->logger->warning('issue while running MetadataDelete', ['exception' => $e]);
}
}
try {
$this->filesMetadataManager->deleteMetadataForFiles($fileIds);
} catch (Exception $e) {
$this->logger->warning('issue while running MetadataDelete', ['exception' => $e]);
}

@ -175,4 +175,30 @@ class IndexRequestService {
$qb->executeStatement();
}
/**
* Drop indexes related to multiple file ids
* if a key is specified, only drop entries related to it
*
* @param int[] $fileIds file ids
* @param string $key metadata key
*
* @throws DbException
*/
public function dropIndexForFiles(array $fileIds, string $key = ''): void {
$chunks = array_chunk($fileIds, 1000);
foreach ($chunks as $chunk) {
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->delete(self::TABLE_METADATA_INDEX)
->where($expr->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)));
if ($key !== '') {
$qb->andWhere($expr->eq('meta_key', $qb->createNamedParameter($key)));
}
$qb->executeStatement();
}
}
}

@ -108,8 +108,10 @@ class MetadataRequestService {
*/
public function getMetadataFromFileIds(array $fileIds): array {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('file_id', 'json', 'sync_token')->from(self::TABLE_METADATA);
$qb->where($qb->expr()->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)));
$qb->select('file_id', 'json', 'sync_token')
->from(self::TABLE_METADATA)
->where($qb->expr()->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)))
->runAcrossAllShards();
$list = [];
$result = $qb->executeQuery();
@ -143,6 +145,22 @@ class MetadataRequestService {
$qb->executeStatement();
}
/**
* @param int[] $fileIds
* @return void
* @throws Exception
*/
public function dropMetadataForFiles(array $fileIds): void {
$chunks = array_chunk($fileIds, 1000);
foreach ($chunks as $chunk) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete(self::TABLE_METADATA)
->where($qb->expr()->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)));
$qb->executeStatement();
}
}
/**
* update metadata in the database
*

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Files\Cache;
use OCP\EventDispatcher\Event;
/**
* Meta-event wrapping multiple CacheEntryRemovedEvent for when an existing
* entry in the cache gets removed.
*
* @since 32.0.0
*/
#[\OCP\AppFramework\Attribute\Listenable(since: '32.0.0')]
class CacheEntriesRemovedEvent extends Event {
/**
* @param CacheEntryRemovedEvent[] $cacheEntryRemovedEvents
*/
public function __construct(
private readonly array $cacheEntryRemovedEvents,
) {
}
/**
* @return CacheEntryRemovedEvent[]
*/
public function getCacheEntryRemovedEvents(): array {
return $this->cacheEntryRemovedEvents;
}
}

@ -11,7 +11,11 @@ namespace OCP\Files\Cache;
/**
* Event for when an existing entry in the cache gets removed
*
* Prefer using \c CacheEntriesRemovedEvent as it is more efficient when deleting
* multiple files at the same time.
*
* @since 21.0.0
* @see CacheEntriesRemovedEvent
*/
class CacheEntryRemovedEvent extends AbstractCacheEvent implements ICacheEvent {
}

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OCP\FilesMetadata;
use OCP\AppFramework\Attribute\Consumable;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Node;
use OCP\FilesMetadata\Exceptions\FilesMetadataException;
@ -20,6 +21,7 @@ use OCP\FilesMetadata\Model\IMetadataValueWrapper;
*
* @since 28.0.0
*/
#[Consumable(since: '28.0.0')]
interface IFilesMetadataManager {
/** @since 28.0.0 */
public const PROCESS_LIVE = 1;
@ -98,6 +100,15 @@ interface IFilesMetadataManager {
*/
public function deleteMetadata(int $fileId): void;
/**
* Delete metadata and its indexes of multiple file ids
*
* @param array<int> $fileIds file ids
* @return void
* @since 32.0.0
*/
public function deleteMetadataForFiles(array $fileIds): void;
/**
* generate and return a MetadataQuery to help building sql queries
*

Loading…
Cancel
Save