1818 * Delete all share entries that have no matching entries in the file cache table.
1919 */
2020class DeleteOrphanedItems extends TimedJob {
21- public const CHUNK_SIZE = 200 ;
21+ public const CHUNK_SIZE = 1000 ;
2222
2323 /**
2424 * sets the correct interval for this timed job
@@ -46,74 +46,81 @@ public function run($argument) {
4646 }
4747
4848 /**
49- * Deleting orphaned system tag mappings
49+ * Delete mapping rows of the 'files' object type whose referenced file no
50+ * longer exists in the file cache.
5051 *
51- * @param string $table
52- * @param string $idCol
53- * @param string $typeCol
52+ * The candidate ids are read from the mapping table itself in keyset-paginated
53+ * chunks and each chunk is checked against the filecache primary key. This
54+ * avoids joining the (potentially huge) mapping table against filecache with a
55+ * GROUP BY, which reads the whole mapping table - and on some databases
56+ * materialises a temp table - on every run even when there are no orphans (the
57+ * common case). It also works the same way whether or not filecache is sharded,
58+ * so a single code path covers both.
59+ *
60+ * @param string $table The mapping table to clean up
61+ * @param string $idCol The column referencing the file id
62+ * @param string $typeCol The column holding the object type
63+ * @param bool $numericId Whether $idCol is an integer column. String columns
64+ * hold the numeric file id as text; the keyset cursor is
65+ * compared (and the rows ordered) using the column's own
66+ * type so the index on $idCol stays usable on every
67+ * database instead of being defeated by an implicit cast.
5468 * @return int Number of deleted entries
5569 */
56- protected function cleanUp (string $ table , string $ idCol , string $ typeCol ): int {
70+ protected function cleanUp (string $ table , string $ idCol , string $ typeCol, bool $ numericId ): int {
5771 $ deletedEntries = 0 ;
5872
5973 $ deleteQuery = $ this ->connection ->getQueryBuilder ();
6074 $ deleteQuery ->delete ($ table )
61- ->where ($ deleteQuery ->expr ()->eq ($ idCol , $ deleteQuery ->createParameter ('objectid ' )));
62-
63- if ($ this ->connection ->getShardDefinition ('filecache ' )) {
64- $ sourceIdChunks = $ this ->getItemIds ($ table , $ idCol , $ typeCol , 1000 );
65- foreach ($ sourceIdChunks as $ sourceIdChunk ) {
66- $ deletedSources = $ this ->findMissingSources ($ sourceIdChunk );
67- $ deleteQuery ->setParameter ('objectid ' , $ deletedSources , IQueryBuilder::PARAM_INT_ARRAY );
68- $ deletedEntries += $ deleteQuery ->executeStatement ();
69- }
70- } else {
71- $ query = $ this ->connection ->getQueryBuilder ();
72- $ query ->select ('t1. ' . $ idCol )
73- ->from ($ table , 't1 ' )
74- ->where ($ query ->expr ()->eq ($ typeCol , $ query ->expr ()->literal ('files ' )))
75- ->leftJoin ('t1 ' , 'filecache ' , 't2 ' , $ query ->expr ()->eq ($ query ->expr ()->castColumn ('t1. ' . $ idCol , IQueryBuilder::PARAM_INT ), 't2.fileid ' ))
76- ->andWhere ($ query ->expr ()->isNull ('t2.fileid ' ))
77- ->groupBy ('t1. ' . $ idCol )
78- ->setMaxResults (self ::CHUNK_SIZE );
79-
80- $ deleteQuery = $ this ->connection ->getQueryBuilder ();
81- $ deleteQuery ->delete ($ table )
82- ->where ($ deleteQuery ->expr ()->in ($ idCol , $ deleteQuery ->createParameter ('objectid ' )));
83-
84- $ deletedInLastChunk = self ::CHUNK_SIZE ;
85- while ($ deletedInLastChunk === self ::CHUNK_SIZE ) {
86- $ chunk = $ query ->executeQuery ()->fetchFirstColumn ();
87- $ deletedInLastChunk = count ($ chunk );
88-
89- $ deleteQuery ->setParameter ('objectid ' , $ chunk , IQueryBuilder::PARAM_INT_ARRAY );
90- $ deletedEntries += $ deleteQuery ->executeStatement ();
75+ ->where ($ deleteQuery ->expr ()->in ($ idCol , $ deleteQuery ->createParameter ('objectid ' )));
76+
77+ foreach ($ this ->getItemIds ($ table , $ idCol , $ typeCol , $ numericId , self ::CHUNK_SIZE ) as $ idChunk ) {
78+ $ missingSources = $ this ->findMissingSources ($ idChunk );
79+ if (count ($ missingSources ) === 0 ) {
80+ continue ;
9181 }
82+
83+ $ deleteQuery ->setParameter ('objectid ' , $ missingSources , IQueryBuilder::PARAM_INT_ARRAY );
84+ $ deletedEntries += $ deleteQuery ->executeStatement ();
9285 }
9386
9487 return $ deletedEntries ;
9588 }
9689
9790 /**
91+ * Yield the distinct 'files' ids of $table in keyset-paginated chunks.
92+ *
93+ * Chunks are ordered by $idCol and advanced with a `$idCol > cursor`
94+ * comparison so the scan stays on the index covering ($typeCol, $idCol). The
95+ * cursor is bound - and the rows therefore ordered and compared - using the
96+ * column's own type: an integer column numerically, a string column lexically.
97+ * Mixing the two (e.g. comparing a varchar column to an integer) would force
98+ * an implicit cast that defeats the index and makes the ordering and the
99+ * comparison disagree, which could skip chunks.
100+ *
98101 * @param string $table
99102 * @param string $idCol
100103 * @param string $typeCol
104+ * @param bool $numericId Whether $idCol is an integer column
101105 * @param int $chunkSize
102106 * @return \Iterator<int[]>
103107 * @throws \OCP\DB\Exception
104108 */
105- private function getItemIds (string $ table , string $ idCol , string $ typeCol , int $ chunkSize ): \Iterator {
109+ private function getItemIds (string $ table , string $ idCol , string $ typeCol , bool $ numericId , int $ chunkSize ): \Iterator {
110+ $ cursorType = $ numericId ? IQueryBuilder::PARAM_INT : IQueryBuilder::PARAM_STR ;
111+
106112 $ query = $ this ->connection ->getQueryBuilder ();
107113 $ query ->select ($ idCol )
108114 ->from ($ table )
109115 ->where ($ query ->expr ()->eq ($ typeCol , $ query ->expr ()->literal ('files ' )))
110- ->groupBy ($ idCol )
111116 ->andWhere ($ query ->expr ()->gt ($ idCol , $ query ->createParameter ('min_id ' )))
117+ ->groupBy ($ idCol )
118+ ->orderBy ($ idCol )
112119 ->setMaxResults ($ chunkSize );
113120
114- $ minId = 0 ;
121+ $ minId = $ numericId ? 0 : ' 0 ' ;
115122 while (true ) {
116- $ query ->setParameter ('min_id ' , $ minId );
123+ $ query ->setParameter ('min_id ' , $ minId, $ cursorType );
117124 $ rows = $ query ->executeQuery ()->fetchFirstColumn ();
118125 if (count ($ rows ) > 0 ) {
119126 $ minId = $ rows [count ($ rows ) - 1 ];
@@ -139,7 +146,7 @@ private function findMissingSources(array $ids): array {
139146 * @return int Number of deleted entries
140147 */
141148 protected function cleanSystemTags () {
142- $ deletedEntries = $ this ->cleanUp ('systemtag_object_mapping ' , 'objectid ' , 'objecttype ' );
149+ $ deletedEntries = $ this ->cleanUp ('systemtag_object_mapping ' , 'objectid ' , 'objecttype ' , false );
143150 $ this ->logger ->debug ("$ deletedEntries orphaned system tag relations deleted " , ['app ' => 'DeleteOrphanedItems ' ]);
144151 return $ deletedEntries ;
145152 }
@@ -150,7 +157,7 @@ protected function cleanSystemTags() {
150157 * @return int Number of deleted entries
151158 */
152159 protected function cleanUserTags () {
153- $ deletedEntries = $ this ->cleanUp ('vcategory_to_object ' , 'objid ' , 'type ' );
160+ $ deletedEntries = $ this ->cleanUp ('vcategory_to_object ' , 'objid ' , 'type ' , true );
154161 $ this ->logger ->debug ("$ deletedEntries orphaned user tag relations deleted " , ['app ' => 'DeleteOrphanedItems ' ]);
155162 return $ deletedEntries ;
156163 }
@@ -161,7 +168,7 @@ protected function cleanUserTags() {
161168 * @return int Number of deleted entries
162169 */
163170 protected function cleanComments () {
164- $ deletedEntries = $ this ->cleanUp ('comments ' , 'object_id ' , 'object_type ' );
171+ $ deletedEntries = $ this ->cleanUp ('comments ' , 'object_id ' , 'object_type ' , false );
165172 $ this ->logger ->debug ("$ deletedEntries orphaned comments deleted " , ['app ' => 'DeleteOrphanedItems ' ]);
166173 return $ deletedEntries ;
167174 }
@@ -172,7 +179,7 @@ protected function cleanComments() {
172179 * @return int Number of deleted entries
173180 */
174181 protected function cleanCommentMarkers () {
175- $ deletedEntries = $ this ->cleanUp ('comments_read_markers ' , 'object_id ' , 'object_type ' );
182+ $ deletedEntries = $ this ->cleanUp ('comments_read_markers ' , 'object_id ' , 'object_type ' , false );
176183 $ this ->logger ->debug ("$ deletedEntries orphaned comment read marks deleted " , ['app ' => 'DeleteOrphanedItems ' ]);
177184 return $ deletedEntries ;
178185 }
0 commit comments