1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Elgg\GarbageCollector; |
4
|
|
|
|
5
|
|
|
use Elgg\Application\Database; |
6
|
|
|
use Elgg\Database\Delete; |
7
|
|
|
use Elgg\I18n\Translator; |
8
|
|
|
use Elgg\Traits\Di\ServiceFacade; |
9
|
|
|
use Elgg\Traits\Loggable; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Garbage collecting service |
13
|
|
|
*/ |
14
|
|
|
class GarbageCollector { |
15
|
|
|
|
16
|
|
|
use ServiceFacade; |
17
|
|
|
use Loggable; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var Database |
21
|
|
|
*/ |
22
|
|
|
protected $db; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var Translator |
26
|
|
|
*/ |
27
|
|
|
protected $translator; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $tables; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Constructor |
36
|
|
|
* |
37
|
|
|
* @param Database $db Database |
38
|
|
|
* @param Translator $translator Translator |
39
|
|
|
*/ |
40
|
1 |
|
public function __construct(Database $db, Translator $translator) { |
41
|
1 |
|
$this->db = $db; |
42
|
1 |
|
$this->translator = $translator; |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Returns registered service name |
47
|
|
|
* |
48
|
|
|
* @return string |
49
|
|
|
*/ |
50
|
2280 |
|
public static function name() { |
51
|
2280 |
|
return 'garbagecollector'; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Optimize the database |
56
|
|
|
* |
57
|
|
|
* @param bool $use_logger Add the results to the log (default: false) |
58
|
|
|
* |
59
|
|
|
* @return \stdClass[] |
60
|
|
|
*/ |
61
|
1 |
|
public function optimize(bool $use_logger = false): array { |
62
|
1 |
|
$dbprefix = $this->db->prefix; |
63
|
1 |
|
$output = []; |
64
|
|
|
|
65
|
1 |
|
$output[] = (object) [ |
66
|
1 |
|
'operation' => $this->translator->translate('garbagecollector:start'), |
67
|
1 |
|
'result' => true, |
68
|
1 |
|
'completed' => new \DateTime(), |
69
|
1 |
|
]; |
70
|
|
|
|
71
|
1 |
|
if ($use_logger) { |
72
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:start')); |
73
|
|
|
} |
74
|
|
|
|
75
|
1 |
|
foreach ($this->tables() as $table) { |
76
|
1 |
|
if (stripos($table, "{$dbprefix}system_log_") === 0) { |
77
|
|
|
// rotated system_log tables don't need to be optimized |
78
|
|
|
continue; |
79
|
|
|
} |
80
|
|
|
|
81
|
1 |
|
$result = $this->optimizeTable($table) !== 0; |
82
|
1 |
|
$output[] = (object) [ |
83
|
1 |
|
'operation' => $this->translator->translate('garbagecollector:optimize', [$table]), |
84
|
1 |
|
'result' => $result, |
85
|
1 |
|
'completed' => new \DateTime(), |
86
|
1 |
|
]; |
87
|
|
|
|
88
|
1 |
|
if ($use_logger) { |
89
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:optimize', [$table]) . ' ' . $result ? 'OK' : 'FAILED'); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
1 |
|
$output[] = (object) [ |
94
|
1 |
|
'operation' => $this->translator->translate('garbagecollector:done'), |
95
|
1 |
|
'result' => true, |
96
|
1 |
|
'completed' => new \DateTime(), |
97
|
1 |
|
]; |
98
|
|
|
|
99
|
1 |
|
if ($use_logger) { |
100
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:done')); |
101
|
|
|
} |
102
|
|
|
|
103
|
1 |
|
return $output; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Listen to the garbage collection event |
108
|
|
|
* |
109
|
|
|
* @param \Elgg\Event $event 'gc', 'system' |
110
|
|
|
* |
111
|
|
|
* @return void |
112
|
|
|
*/ |
113
|
|
|
public static function gcCallback(\Elgg\Event $event): void { |
114
|
|
|
$cron_logger = $event->getParam('logger'); |
115
|
|
|
|
116
|
|
|
$instance = self::instance(); |
117
|
|
|
$instance->setLogger($cron_logger); |
118
|
|
|
$instance->cleanupOrphanedData(); |
119
|
|
|
|
120
|
|
|
$cron_logger->notice(elgg_echo('garbagecollector:orphaned:done')); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Go through the database tables and remove orphaned data |
125
|
|
|
* |
126
|
|
|
* @return void |
127
|
|
|
*/ |
128
|
|
|
public function cleanupOrphanedData(): void { |
129
|
|
|
$this->cleanupAccessCollections(); |
130
|
|
|
$this->cleanupAccessCollectionMembership(); |
131
|
|
|
$this->cleanupAnnotations(); |
132
|
|
|
$this->cleanupDelayedEmailQueue(); |
133
|
|
|
$this->cleanupEntityRelationships(); |
134
|
|
|
$this->cleanupMetadata(); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Get a list of DB tables |
139
|
|
|
* |
140
|
|
|
* @return array |
141
|
|
|
*/ |
142
|
1 |
|
protected function tables(): array { |
143
|
1 |
|
if (isset($this->tables)) { |
144
|
|
|
return $this->tables; |
145
|
|
|
} |
146
|
|
|
|
147
|
1 |
|
$table_prefix = $this->db->prefix; |
148
|
1 |
|
$result = $this->db->getConnection('read')->executeQuery("SHOW TABLES LIKE '{$table_prefix}%'"); |
149
|
|
|
|
150
|
1 |
|
$this->tables = []; |
151
|
|
|
|
152
|
1 |
|
$rows = $result->fetchAllAssociative(); |
153
|
1 |
|
foreach ($rows as $row) { |
154
|
1 |
|
if (empty($row)) { |
155
|
|
|
continue; |
156
|
|
|
} |
157
|
|
|
|
158
|
1 |
|
foreach ($row as $element) { |
159
|
1 |
|
$this->tables[] = $element; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
1 |
|
return $this->tables; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Optimize table |
168
|
|
|
* |
169
|
|
|
* @param string $table Table |
170
|
|
|
* |
171
|
|
|
* @return int |
172
|
|
|
*/ |
173
|
1 |
|
protected function optimizeTable(string $table): int { |
174
|
1 |
|
$result = $this->db->getConnection('write')->executeQuery("OPTIMIZE TABLE {$table}"); |
175
|
1 |
|
return $result->rowCount(); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Remove access collections where (AND): |
180
|
|
|
* - owner_guid no longer exist as guid in the entities table |
181
|
|
|
* - id is not used in the entities table as access_id |
182
|
|
|
* - id is not used in the annotations table as access_id |
183
|
|
|
* |
184
|
|
|
* @return void |
185
|
|
|
*/ |
186
|
|
|
protected function cleanupAccessCollections(): void { |
187
|
|
|
$delete = Delete::fromTable('access_collections'); |
188
|
|
|
|
189
|
|
|
$owner_sub = $delete->subquery('entities'); |
190
|
|
|
$owner_sub->select('guid'); |
191
|
|
|
|
192
|
|
|
$entities_access_id_sub = $delete->subquery('entities'); |
193
|
|
|
$entities_access_id_sub->select('DISTINCT access_id'); |
194
|
|
|
|
195
|
|
|
$annotations_access_id_sub = $delete->subquery('annotations'); |
196
|
|
|
$annotations_access_id_sub->select('DISTINCT access_id'); |
197
|
|
|
|
198
|
|
|
$delete->where($delete->merge([ |
199
|
|
|
$delete->compare('owner_guid', 'not in', $owner_sub->getSQL()), |
200
|
|
|
$delete->compare('id', 'not in', $entities_access_id_sub->getSQL()), |
201
|
|
|
$delete->compare('id', 'not in', $annotations_access_id_sub->getSQL()), |
202
|
|
|
], 'AND')); |
203
|
|
|
|
204
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:orphaned', ['access_collections']) . ': ' . $this->db->deleteData($delete)); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Remove access collection memberships where (OR): |
209
|
|
|
* - user_guid no longer exists in the entities table |
210
|
|
|
* - access_collection_id no longer exists in the access_collections table |
211
|
|
|
* |
212
|
|
|
* @return void |
213
|
|
|
*/ |
214
|
|
|
protected function cleanupAccessCollectionMembership(): void { |
215
|
|
|
$delete = Delete::fromTable('access_collection_membership'); |
216
|
|
|
|
217
|
|
|
$user_sub = $delete->subquery('entities'); |
218
|
|
|
$user_sub->select('guid'); |
219
|
|
|
|
220
|
|
|
$access_collection_sub = $delete->subquery('access_collections'); |
221
|
|
|
$access_collection_sub->select('id'); |
222
|
|
|
|
223
|
|
|
$delete->where($delete->merge([ |
224
|
|
|
$delete->compare('user_guid', 'not in', $user_sub->getSQL()), |
225
|
|
|
$delete->compare('access_collection_id', 'not in', $access_collection_sub->getSQL()), |
226
|
|
|
], 'OR')); |
227
|
|
|
|
228
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:orphaned', ['access_collection_membership']) . ': ' . $this->db->deleteData($delete)); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Remove annotations where: |
233
|
|
|
* - entity_guid no longer exists in the entities table |
234
|
|
|
* |
235
|
|
|
* @return void |
236
|
|
|
*/ |
237
|
|
|
protected function cleanupAnnotations(): void { |
238
|
|
|
$delete = Delete::fromTable('annotations'); |
239
|
|
|
|
240
|
|
|
$entity_sub = $delete->subquery('entities'); |
241
|
|
|
$entity_sub->select('guid'); |
242
|
|
|
|
243
|
|
|
$delete->where($delete->compare('entity_guid', 'not in', $entity_sub->getSQL())); |
244
|
|
|
|
245
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:orphaned', ['annotations']) . ': ' . $this->db->deleteData($delete)); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Remove delayed emails where: |
250
|
|
|
* - recipient_guid no longer exists in the entities table |
251
|
|
|
* |
252
|
|
|
* @return void |
253
|
|
|
*/ |
254
|
|
|
protected function cleanupDelayedEmailQueue(): void { |
255
|
|
|
$delete = Delete::fromTable('delayed_email_queue'); |
256
|
|
|
|
257
|
|
|
$entity_sub = $delete->subquery('entities'); |
258
|
|
|
$entity_sub->select('guid'); |
259
|
|
|
|
260
|
|
|
$delete->where($delete->compare('recipient_guid', 'not in', $entity_sub->getSQL())); |
261
|
|
|
|
262
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:orphaned', ['delayed_email_queue']) . ': ' . $this->db->deleteData($delete)); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Remove entity relationships where (OR): |
267
|
|
|
* - guid_one no longer exists in the entities table |
268
|
|
|
* - guid_two no longer exists in the entities table |
269
|
|
|
* |
270
|
|
|
* @return void |
271
|
|
|
*/ |
272
|
|
|
protected function cleanupEntityRelationships(): void { |
273
|
|
|
$delete = Delete::fromTable('entity_relationships'); |
274
|
|
|
|
275
|
|
|
$guid_sub = $delete->subquery('entities'); |
276
|
|
|
$guid_sub->select('guid'); |
277
|
|
|
|
278
|
|
|
$delete->where($delete->merge([ |
279
|
|
|
$delete->compare('guid_one', 'not in', $guid_sub->getSQL()), |
280
|
|
|
$delete->compare('guid_two', 'not in', $guid_sub->getSQL()), |
281
|
|
|
], 'OR')); |
282
|
|
|
|
283
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:orphaned', ['entity_relationships']) . ': ' . $this->db->deleteData($delete)); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Remove metadata where: |
288
|
|
|
* - entity_guid no longer exists in the entities table |
289
|
|
|
* |
290
|
|
|
* @return void |
291
|
|
|
*/ |
292
|
|
|
protected function cleanupMetadata(): void { |
293
|
|
|
$delete = Delete::fromTable('metadata'); |
294
|
|
|
|
295
|
|
|
$entity_guid_sub = $delete->subquery('entities'); |
296
|
|
|
$entity_guid_sub->select('guid'); |
297
|
|
|
|
298
|
|
|
$delete->where($delete->compare('entity_guid', 'not in', $entity_guid_sub->getSQL())); |
299
|
|
|
|
300
|
|
|
$this->getLogger()->notice($this->translator->translate('garbagecollector:orphaned', ['metadata']) . ': ' . $this->db->deleteData($delete)); |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|