mirror of
https://github.com/nextcloud/server.git
synced 2026-06-12 10:10:49 -04:00
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>
This commit is contained in:
parent
ebfdbf86b9
commit
fd3878448b
13 changed files with 182 additions and 19 deletions
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
35
lib/public/Files/Cache/CacheEntriesRemovedEvent.php
Normal file
35
lib/public/Files/Cache/CacheEntriesRemovedEvent.php
Normal file
|
|
@ -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…
Reference in a new issue