Passed
Push — 5.x ( 88ff80...0e9d8a )
by Jeroen
12:39 queued 15s
created

GarbageCollector::optimize()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 7.1284

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 16
nop 1
dl 0
loc 43
ccs 25
cts 29
cp 0.8621
crap 7.1284
rs 8.5866
c 0
b 0
f 0
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