Skip to content

Commit 6fa4266

Browse files
authored
Merge pull request #48563 from nextcloud/metadata-storage-id
Fix metadata storage with sharding
2 parents 31f4f67 + b21c026 commit 6fa4266

File tree

6 files changed

+149
-0
lines changed

6 files changed

+149
-0
lines changed

apps/dav/lib/Connector/Sabre/FilesPlugin.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OCA\DAV\Connector\Sabre;
99

1010
use OC\AppFramework\Http\Request;
11+
use OC\FilesMetadata\Model\FilesMetadata;
1112
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
1213
use OCP\Constants;
1314
use OCP\Files\ForbiddenException;
@@ -575,7 +576,9 @@ private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node
575576
$propPatch->handle(
576577
$mutation,
577578
function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
579+
/** @var FilesMetadata $metadata */
578580
$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
581+
$metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
579582
$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
580583

581584
// confirm metadata key is editable via PROPPATCH

apps/files/lib/Listener/SyncLivePhotosListener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace OCA\Files\Listener;
1010

11+
use OC\FilesMetadata\Model\FilesMetadata;
1112
use OCA\Files\Service\LivePhotosService;
1213
use OCP\EventDispatcher\Event;
1314
use OCP\EventDispatcher\IEventListener;
@@ -154,10 +155,14 @@ private function handleCopy(NodeCopiedEvent $event, Node $peerFile): void {
154155
* We have everything to update metadata and keep the link between the 2 copies.
155156
*/
156157
$newPeerFile = $peerFile->copy($targetParent->getPath() . '/' . $peerTargetName);
158+
/** @var FilesMetadata $targetMetadata */
157159
$targetMetadata = $this->filesMetadataManager->getMetadata($targetFile->getId(), true);
160+
$targetMetadata->setStorageId($targetFile->getStorage()->getCache()->getNumericStorageId());
158161
$targetMetadata->setString('files-live-photo', (string)$newPeerFile->getId());
159162
$this->filesMetadataManager->saveMetadata($targetMetadata);
163+
/** @var FilesMetadata $peerMetadata */
160164
$peerMetadata = $this->filesMetadataManager->getMetadata($newPeerFile->getId(), true);
165+
$peerMetadata->setStorageId($newPeerFile->getStorage()->getCache()->getNumericStorageId());
161166
$peerMetadata->setString('files-live-photo', (string)$targetFile->getId());
162167
$this->filesMetadataManager->saveMetadata($peerMetadata);
163168
}

lib/private/FilesMetadata/FilesMetadataManager.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,14 @@ public function refreshMetadata(
7777
int $process = self::PROCESS_LIVE,
7878
string $namedEvent = '',
7979
): IFilesMetadata {
80+
$storageId = $node->getStorage()->getCache()->getNumericStorageId();
8081
try {
82+
/** @var FilesMetadata $metadata */
8183
$metadata = $this->metadataRequestService->getMetadataFromFileId($node->getId());
8284
} catch (FilesMetadataNotFoundException) {
8385
$metadata = new FilesMetadata($node->getId());
8486
}
87+
$metadata->setStorageId($storageId);
8588

8689
// if $process is LIVE, we enforce LIVE
8790
// if $process is NAMED, we go NAMED

lib/private/FilesMetadata/Model/FilesMetadata.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class FilesMetadata implements IFilesMetadata {
2727
private bool $updated = false;
2828
private int $lastUpdate = 0;
2929
private string $syncToken = '';
30+
private ?int $storageId = null;
3031

3132
public function __construct(
3233
private int $fileId = 0,
@@ -42,6 +43,22 @@ public function getFileId(): int {
4243
return $this->fileId;
4344
}
4445

46+
public function getStorageId(): ?int {
47+
return $this->storageId;
48+
}
49+
50+
/**
51+
* Set which storage the file this metadata belongs to.
52+
*
53+
* This helps with sharded filecache setups to know where to store the metadata
54+
*
55+
* @param int $storageId
56+
* @return void
57+
*/
58+
public function setStorageId(int $storageId): void {
59+
$this->storageId = $storageId;
60+
}
61+
4562
/**
4663
* @inheritDoc
4764
* @return int timestamp

lib/private/FilesMetadata/Service/MetadataRequestService.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ public function __construct(
2828
) {
2929
}
3030

31+
private function getStorageId(IFilesMetadata $filesMetadata): int {
32+
if ($filesMetadata instanceof FilesMetadata) {
33+
$storage = $filesMetadata->getStorageId();
34+
if ($storage) {
35+
return $storage;
36+
}
37+
}
38+
// all code paths that lead to saving metadata *should* have the storage id set
39+
// this fallback is there just in case
40+
$query = $this->dbConnection->getQueryBuilder();
41+
$query->select('storage')
42+
->from('filecache')
43+
->where($query->expr()->eq('fileid', $query->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)));
44+
$storageId = $query->executeQuery()->fetchColumn();
45+
46+
if ($filesMetadata instanceof FilesMetadata) {
47+
$filesMetadata->setStorageId($storageId);
48+
}
49+
return $storageId;
50+
}
51+
3152
/**
3253
* store metadata into database
3354
*
@@ -38,6 +59,7 @@ public function __construct(
3859
public function store(IFilesMetadata $filesMetadata): void {
3960
$qb = $this->dbConnection->getQueryBuilder();
4061
$qb->insert(self::TABLE_METADATA)
62+
->hintShardKey('storage', $this->getStorageId($filesMetadata))
4163
->setValue('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT))
4264
->setValue('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
4365
->setValue('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
@@ -134,6 +156,7 @@ public function updateMetadata(IFilesMetadata $filesMetadata): int {
134156
$expr = $qb->expr();
135157

136158
$qb->update(self::TABLE_METADATA)
159+
->hintShardKey('files_metadata', $this->getStorageId($filesMetadata))
137160
->set('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
138161
->set('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
139162
->set('last_update', $qb->createFunction('NOW()'))
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2024 Robin Appelman <[email protected]>
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace Test\FilesMetadata;
10+
11+
use OC\BackgroundJob\JobList;
12+
use OC\Files\Storage\Temporary;
13+
use OC\FilesMetadata\FilesMetadataManager;
14+
use OC\FilesMetadata\Service\IndexRequestService;
15+
use OC\FilesMetadata\Service\MetadataRequestService;
16+
use OCP\EventDispatcher\Event;
17+
use OCP\EventDispatcher\IEventDispatcher;
18+
use OCP\Files\Folder;
19+
use OCP\Files\IRootFolder;
20+
use OCP\FilesMetadata\AMetadataEvent;
21+
use OCP\IAppConfig;
22+
use OCP\IDBConnection;
23+
use OCP\Server;
24+
use Psr\Log\LoggerInterface;
25+
use Test\TestCase;
26+
use Test\Traits\MountProviderTrait;
27+
use Test\Traits\UserTrait;
28+
29+
/**
30+
* @group DB
31+
*/
32+
class FilesMetadataManagerTest extends TestCase {
33+
use UserTrait;
34+
use MountProviderTrait;
35+
36+
private IEventDispatcher $eventDispatcher;
37+
private JobList $jobList;
38+
private IAppConfig $appConfig;
39+
private LoggerInterface $logger;
40+
private MetadataRequestService $metadataRequestService;
41+
private IndexRequestService $indexRequestService;
42+
private FilesMetadataManager $manager;
43+
private IDBConnection $connection;
44+
private Folder $userFolder;
45+
private array $metadata = [];
46+
47+
protected function setUp(): void {
48+
parent::setUp();
49+
50+
$this->jobList = $this->createMock(JobList::class);
51+
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
52+
$this->eventDispatcher->method('dispatchTyped')->willReturnCallback(function (Event $event) {
53+
if ($event instanceof AMetadataEvent) {
54+
$name = $event->getNode()->getName();
55+
if (isset($this->metadata[$name])) {
56+
$meta = $event->getMetadata();
57+
foreach ($this->metadata[$name] as $key => $value) {
58+
$meta->setString($key, $value);
59+
}
60+
}
61+
}
62+
});
63+
$this->appConfig = $this->createMock(IAppConfig::class);
64+
$this->logger = $this->createMock(LoggerInterface::class);
65+
66+
$this->connection = Server::get(IDBConnection::class);
67+
$this->metadataRequestService = new MetadataRequestService($this->connection, $this->logger);
68+
$this->indexRequestService = new IndexRequestService($this->connection, $this->logger);
69+
$this->manager = new FilesMetadataManager(
70+
$this->eventDispatcher,
71+
$this->jobList,
72+
$this->appConfig,
73+
$this->logger,
74+
$this->metadataRequestService,
75+
$this->indexRequestService,
76+
);
77+
78+
$this->createUser('metatest', '');
79+
$this->registerMount('metatest', new Temporary([]), '/metatest');
80+
81+
$rootFolder = Server::get(IRootFolder::class);
82+
$this->userFolder = $rootFolder->getUserFolder('metatest');
83+
}
84+
85+
public function testRefreshMetadata(): void {
86+
$this->metadata['test.txt'] = [
87+
'istest' => 'yes'
88+
];
89+
$file = $this->userFolder->newFile('test.txt', 'test');
90+
$stored = $this->manager->refreshMetadata($file);
91+
$this->assertEquals($file->getId(), $stored->getFileId());
92+
$this->assertEquals('yes', $stored->getString('istest'));
93+
94+
$retrieved = $this->manager->getMetadata($file->getId());
95+
$this->assertEquals($file->getId(), $retrieved->getFileId());
96+
$this->assertEquals('yes', $retrieved->getString('istest'));
97+
}
98+
}

0 commit comments

Comments
 (0)