1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files_Sharing;
use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use PDO;
use Psr\Log\LoggerInterface;
use function array_map;
/**
* Delete all share entries that have no matching entries in the file cache table.
*/
class DeleteOrphanedSharesJob extends TimedJob {
use TTransactional;
private const CHUNK_SIZE = 1000;
private const INTERVAL = 24 * 60 * 60;
/**
* sets the correct interval for this timed job
*/
public function __construct(
ITimeFactory $time,
private IDBConnection $db,
private LoggerInterface $logger,
) {
parent::__construct($time);
$this->setInterval(self::INTERVAL); // 1 day
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
/**
* Makes the background job do its work
*
* @param array $argument unused argument
*/
public function run($argument) {
if ($this->db->getShardDefinition('filecache')) {
$this->shardingCleanup();
return;
}
$qbSelect = $this->db->getQueryBuilder();
$qbSelect->select('id')
->from('share', 's')
->leftJoin('s', 'filecache', 'fc', $qbSelect->expr()->eq('s.file_source', 'fc.fileid'))
->where($qbSelect->expr()->isNull('fc.fileid'))
->setMaxResults(self::CHUNK_SIZE);
$deleteQb = $this->db->getQueryBuilder();
$deleteQb->delete('share')
->where(
$deleteQb->expr()->in('id', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)
);
/**
* Read a chunk of orphan rows and delete them. Continue as long as the
* chunk is filled and time before the next cron run does not run out.
*
* Note: With isolation level READ COMMITTED, the database will allow
* other transactions to delete rows between our SELECT and DELETE. In
* that (unlikely) case, our DELETE will have fewer affected rows than
* IDs passed for the WHERE IN. If this happens while processing a full
* chunk, the logic below will stop prematurely.
* Note: The queries below are optimized for low database locking. They
* could be combined into one single DELETE with join or sub query, but
* that has shown to (dead)lock often.
*/
$cutOff = $this->time->getTime() + self::INTERVAL;
do {
$deleted = $this->atomic(function () use ($qbSelect, $deleteQb) {
$result = $qbSelect->executeQuery();
$ids = array_map('intval', $result->fetchAll(PDO::FETCH_COLUMN));
$result->closeCursor();
$deleteQb->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY);
$deleted = $deleteQb->executeStatement();
$this->logger->debug('{deleted} orphaned share(s) deleted', [
'app' => 'DeleteOrphanedSharesJob',
'deleted' => $deleted,
]);
return $deleted;
}, $this->db);
} while ($deleted >= self::CHUNK_SIZE && $this->time->getTime() <= $cutOff);
}
private function shardingCleanup(): void {
$qb = $this->db->getQueryBuilder();
$qb->selectDistinct('file_source')
->from('share', 's');
$sourceFiles = $qb->executeQuery()->fetchAll(PDO::FETCH_COLUMN);
$deleteQb = $this->db->getQueryBuilder();
$deleteQb->delete('share')
->where(
$deleteQb->expr()->in('file_source', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)
);
$chunks = array_chunk($sourceFiles, self::CHUNK_SIZE);
foreach ($chunks as $chunk) {
$deletedFiles = $this->findMissingSources($chunk);
$this->atomic(function () use ($deletedFiles, $deleteQb) {
$deleteQb->setParameter('ids', $deletedFiles, IQueryBuilder::PARAM_INT_ARRAY);
$deleted = $deleteQb->executeStatement();
$this->logger->debug('{deleted} orphaned share(s) deleted', [
'app' => 'DeleteOrphanedSharesJob',
'deleted' => $deleted,
]);
return $deleted;
}, $this->db);
}
}
private function findMissingSources(array $ids): array {
$qb = $this->db->getQueryBuilder();
$qb->select('fileid')
->from('filecache')
->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
$found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
return array_diff($ids, $found);
}
}
|