|
30 | 30 | use OCP\AppFramework\Db\TTransactional; |
31 | 31 | use OCP\AppFramework\Utility\ITimeFactory; |
32 | 32 | use OCP\BackgroundJob\TimedJob; |
| 33 | +use OCP\DB\QueryBuilder\IQueryBuilder; |
| 34 | +use OCP\IDBConnection; |
| 35 | +use PDO; |
| 36 | +use Psr\Log\LoggerInterface; |
| 37 | +use function array_map; |
33 | 38 |
|
34 | 39 | /** |
35 | 40 | * Delete all share entries that have no matching entries in the file cache table. |
@@ -69,15 +74,45 @@ public function __construct( |
69 | 74 | * @param array $argument unused argument |
70 | 75 | */ |
71 | 76 | public function run($argument) { |
72 | | - $connection = \OC::$server->getDatabaseConnection(); |
73 | | - $logger = \OC::$server->getLogger(); |
| 77 | + $qbSelect = $this->db->getQueryBuilder(); |
| 78 | + $qbSelect->select('id') |
| 79 | + ->from('share', 's') |
| 80 | + ->leftJoin('s', 'filecache', 'fc', $qbSelect->expr()->eq('s.file_source', 'fc.fileid')) |
| 81 | + ->where($qbSelect->expr()->isNull('fc.fileid')) |
| 82 | + ->setMaxResults(self::CHUNK_SIZE); |
| 83 | + $deleteQb = $this->db->getQueryBuilder(); |
| 84 | + $deleteQb->delete('share') |
| 85 | + ->where( |
| 86 | + $deleteQb->expr()->in('id', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY) |
| 87 | + ); |
74 | 88 |
|
75 | | - $sql = |
76 | | - 'DELETE FROM `*PREFIX*share` ' . |
77 | | - 'WHERE `item_type` in (\'file\', \'folder\') ' . |
78 | | - 'AND NOT EXISTS (SELECT `fileid` FROM `*PREFIX*filecache` WHERE `file_source` = `fileid`)'; |
79 | | - |
80 | | - $deletedEntries = $connection->executeUpdate($sql); |
81 | | - $logger->debug("$deletedEntries orphaned share(s) deleted", ['app' => 'DeleteOrphanedSharesJob']); |
| 89 | + /** |
| 90 | + * Read a chunk of orphan rows and delete them. Continue as long as the |
| 91 | + * chunk is filled and time before the next cron run does not run out. |
| 92 | + * |
| 93 | + * Note: With isolation level READ COMMITTED, the database will allow |
| 94 | + * other transactions to delete rows between our SELECT and DELETE. In |
| 95 | + * that (unlikely) case, our DELETE will have fewer affected rows than |
| 96 | + * IDs passed for the WHERE IN. If this happens while processing a full |
| 97 | + * chunk, the logic below will stop prematurely. |
| 98 | + * Note: The queries below are optimized for low database locking. They |
| 99 | + * could be combined into one single DELETE with join or sub query, but |
| 100 | + * that has shown to (dead)lock often. |
| 101 | + */ |
| 102 | + $cutOff = $this->time->getTime() + self::INTERVAL; |
| 103 | + do { |
| 104 | + $deleted = $this->atomic(function () use ($qbSelect, $deleteQb) { |
| 105 | + $result = $qbSelect->executeQuery(); |
| 106 | + $ids = array_map('intval', $result->fetchAll(PDO::FETCH_COLUMN)); |
| 107 | + $result->closeCursor(); |
| 108 | + $deleteQb->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY); |
| 109 | + $deleted = $deleteQb->executeStatement(); |
| 110 | + $this->logger->debug("{deleted} orphaned share(s) deleted", [ |
| 111 | + 'app' => 'DeleteOrphanedSharesJob', |
| 112 | + 'deleted' => $deleted, |
| 113 | + ]); |
| 114 | + return $deleted; |
| 115 | + }, $this->db); |
| 116 | + } while ($deleted >= self::CHUNK_SIZE && $this->time->getTime() <= $cutOff); |
82 | 117 | } |
83 | 118 | } |
0 commit comments