Passed
Pull Request — master (#44)
by Mischa ter
02:24
created

QueueShell::initialize()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 50
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
cc 7
eloc 30
c 6
b 1
f 0
nc 24
nop 0
dl 0
loc 50
rs 8.5066
1
<?php
2
App::uses('Folder', 'Utility');
3
App::uses('QueuedTask', 'Model');
4
App::uses('AppShell', 'Console/Command');
5
App::uses('CakeText', 'Utility');
6
7
declare(ticks = 1);
8
9
/**
10
 * Queue Shell.
11
 *
12
 * @property Queue.QueuedTask $QueuedTask
13
 */
0 ignored issues
show
Documentation Bug introduced by
The doc comment Queue.QueuedTask at position 0 could not be parsed: Unknown type name 'Queue.QueuedTask' at position 0 in Queue.QueuedTask.
Loading history...
14
class QueueShell extends AppShell {
0 ignored issues
show
Bug introduced by
The type AppShell was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
16
/**
17
 * An array of names of models to load.
18
 *
19
 * @var array
20
 */
21
	public $uses = ['Queue.QueuedTask'];
22
23
/**
24
 * A list of available queue tasks and their individual configurations.
25
 *
26
 * @var array
27
 */
28
	protected $_taskConf;
29
30
/**
31
 * Indicates whether or not the worker should exit on next the iteration.
32
 *
33
 * @var bool
34
 */
35
	private $__exit;
36
37
/**
38
 * Overwrite shell initialize to dynamically load all queue related tasks.
39
 *
40
 * @return void
41
 */
42
	public function initialize() {
43
		// Check for tasks inside plugins and application
44
		$paths = App::path('Console/Command/Task');
45
46
		foreach ($paths as $path) {
47
			$Folder = new Folder($path);
0 ignored issues
show
Bug introduced by
The type Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
48
			$res = array_merge($this->tasks, $Folder->find('Queue.*\.php'));
49
			foreach ($res as &$r) {
50
				$r = basename($r, 'Task.php');
51
			}
52
53
			$this->tasks = $res;
0 ignored issues
show
Bug Best Practice introduced by
The property tasks does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
54
		}
55
56
		$plugins = App::objects('plugin');
57
		foreach ($plugins as $plugin) {
58
			$pluginPaths = App::path('Console/Command/Task', $plugin);
59
			foreach ($pluginPaths as $pluginPath) {
60
				$Folder = new Folder($pluginPath);
61
				$res = $Folder->find('Queue.*Task\.php');
62
				foreach ($res as &$r) {
63
					$r = $plugin . '.' . basename($r, 'Task.php');
64
				}
65
66
				$this->tasks = array_merge($this->tasks, $res);
67
			}
68
		}
69
70
		$conf = Configure::read('Queue');
0 ignored issues
show
Bug introduced by
The type Configure was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
71
		if (!is_array($conf)) {
72
			$conf = [];
73
		}
74
75
		// Merge with default configuration vars.
76
		Configure::write('Queue', array_merge(
77
				[
78
					'workers' => 3,
79
					'sleepTime' => 10,
80
					'gcprop' => 10,
81
					'defaultWorkerTimeout' => 2 * MINUTE,
0 ignored issues
show
Bug introduced by
The constant MINUTE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
82
					'defaultWorkerRetries' => 4,
83
					'workerMaxRuntime' => 0,
84
					'cleanupTimeout' => DAY,
0 ignored issues
show
Bug introduced by
The constant DAY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
85
					'exitWhenNothingToDo' => false
86
				],
87
				$conf
88
			)
89
		);
90
91
		parent::initialize();
92
	}
93
94
/**
95
 * Gets and configures the option parser.
96
 *
97
 * @return ConsoleOptionParser
0 ignored issues
show
Bug introduced by
The type ConsoleOptionParser was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
98
 */
99
	public function getOptionParser() {
100
		$parser = parent::getOptionParser();
101
		$parser->addSubcommand('add', [
102
			'help' => __d('queue', 'Tries to call the cli `add()` function on a task.'),
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

102
			'help' => /** @scrutinizer ignore-call */ __d('queue', 'Tries to call the cli `add()` function on a task.'),
Loading history...
103
			'parser' => [
104
				'description' => [
105
					__d('queue', 'Tries to call the cli `add()` function on a task.'),
106
					__d('queue', 'Tasks may or may not provide this functionality.')
107
				],
108
				'arguments' => [
109
					'taskname' => [
110
						'help' => __d('queue', 'Name of the task.'),
111
						'required' => true,
112
						'choices' => $this->taskNames
113
					],
114
					'taskdata' => [
115
						'help' => __d('queue', 'Data needed by task.'),
116
						'required' => false,
117
					]
118
				]
119
			]
120
		])->addSubcommand('runworker', [
121
			'help' => __d('queue', 'Run a queue worker.'),
122
			'parser' => [
123
				'description' => [__d('queue', 'Run a queue worker, which will look for a pending task it can execute.')],
124
				'options' => [
125
					'type' => [
126
						'short' => 't',
127
						'help' => 'Type (comma separated list possible)',
128
						'default' => null
129
					]
130
				]
131
			]
132
		])->addSubcommand('stats', [
133
			'help' => __d('queue', 'Display general statistics.'),
134
			'parser' => [
135
				'description' => __d('queue', 'Display general statistics.'),
136
			]
137
		])->addSubcommand('clean', [
138
			'help' => __d('queue', 'Manually call cleanup function to delete task data of completed tasks.'),
139
			'parser' => [
140
				'description' => __d('queue', 'Manually call cleanup function to delete task data of completed tasks.')
141
			]
142
		])->addSubcommand('clean_failed', [
143
			'help' => __d('queue', 'Manually call cleanup function to delete task data of failed tasks.'),
144
			'parser' => [
145
				'description' => __d('queue', 'Manually call cleanup function to delete task data of failed tasks.')
146
			]
147
		])->description(__d('queue', 'CakePHP Queue Plugin.'));
148
149
		return $parser;
150
	}
151
152
/**
153
 * Looks for a queue task of the passed name and try to call add() on it.
154
 *
155
 *	A queue task may provide an add function to enable the user to create new tasks via commandline.
156
 *
157
 * @return void
158
 */
159
	public function add() {
160
		$name = Inflector::camelize($this->args[0]);
0 ignored issues
show
Bug introduced by
The type Inflector was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
161
162
		if (in_array($name, $this->taskNames)) {
163
			$this->{$name}->add();
164
		} elseif (in_array('Queue' . $name, $this->taskNames)) {
165
			$this->{'Queue' . $name}->add();
166
		} else {
167
			$this->out(__d('queue', 'Error: Task not Found: %s', $name));
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

167
			$this->out(/** @scrutinizer ignore-call */ __d('queue', 'Error: Task not Found: %s', $name));
Loading history...
168
			$this->out('Available Tasks:');
169
			foreach ($this->taskNames as $loadedTask) {
170
				$this->out(' * ' . $this->_taskName($loadedTask));
171
			}
172
		}
173
	}
174
175
/**
176
 * Output the task without Queue or Task
177
 * example: QueueImageTask becomes Image on display
178
 *
179
 * @param string $task A task name
180
 * @return string Cleaned task name
181
 */
182
	protected function _taskName($task) {
183
		if (strpos($task, 'Queue') === 0) {
184
			return substr($task, 5);
185
		}
186
187
		return $task;
188
	}
189
190
/**
191
 * Run a queue worker loop.
192
 *
193
 *	Runs a queue worker process which will try to find unassigned tasks in the queue
194
 *	which it may run and try to fetch and execute them.
195
 *
196
 * @return void
197
 */
198
	public function runworker() {
199
		// Enable garbage collector (PHP >= 5.3)
200
		if (function_exists('gc_enable')) {
201
			gc_enable();
202
		}
203
204
		// Register signal handler(s) if possible
205
		if (function_exists('pcntl_signal')) {
206
			pcntl_signal(SIGTERM, [$this, 'signalHandler']);
207
			pcntl_signal(SIGINT, [$this, 'signalHandler']);
208
		} else {
209
			$this->err(__d('queue', 'Signal handler(s) could not be registered.'));
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

209
			$this->err(/** @scrutinizer ignore-call */ __d('queue', 'Signal handler(s) could not be registered.'));
Loading history...
210
		}
211
212
		$this->__exit = false;
213
214
		$workerStartTime = time();
215
216
		$typesParam = $this->param('type');
217
		$types = is_string($typesParam) ? $this->_stringToArray($typesParam) : [];
218
219
		while (!$this->__exit) {
220
			$this->out(__d('queue', 'Looking for a job.'), 1, Shell::VERBOSE);
0 ignored issues
show
Bug introduced by
The type Shell was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
221
222
			$data = $this->QueuedTask->requestJob($this->_getTaskConf(), $types);
223
			if ($this->QueuedTask->exit === true) {
224
				$this->__exit = true;
225
			} else {
226
				if ($data !== false) {
227
					$jobId = $data['id'];
228
					$taskname = 'Queue' . $data['task'];
229
					$this->out(__d('queue', 'Running job of task \'%s\' \'%d\'.', $data['task'], $jobId));
230
231
					$taskStartTime = time();
232
					$return = $this->{$taskname}->run(unserialize($data['data']));
233
					$took = time() - $taskStartTime;
234
					if ($return) {
235
						$this->QueuedTask->markJobDone($jobId);
236
						$this->out(
237
							__d(
238
								'queue',
239
								'Job \'%d\' finished (took %s).',
240
								$jobId,
241
								__dn('queue', '%d second', '%d seconds', $took, $took)
0 ignored issues
show
Bug introduced by
The function __dn was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

241
								/** @scrutinizer ignore-call */ 
242
        __dn('queue', '%d second', '%d seconds', $took, $took)
Loading history...
242
							)
243
						);
244
					} else {
245
						$failureMessage = null;
246
						if (isset($this->{$taskname}->failureMessage) && !empty($this->{$taskname}->failureMessage)) {
247
							$failureMessage = $this->{$taskname}->failureMessage;
248
						}
249
						$this->QueuedTask->markJobFailed($jobId, $failureMessage);
250
						$this->out(__d('queue', 'Job \'%d\' did not finish, requeued.', $jobId));
251
					}
252
				} elseif (Configure::read('Queue.exitWhenNothingToDo')) {
253
					$this->out(__d('queue', 'Nothing to do, exiting.'));
254
					$this->__exit = true;
255
				} else {
256
					$this->out(
257
						__d('queue', 'Nothing to do, sleeping for %d second(s).', Configure::read('Queue.sleepTime')),
258
						1, Shell::VERBOSE
259
					);
260
261
					sleep(Configure::read('Queue.sleepTime'));
262
				}
263
264
				// Check if we are over the maximum runtime and end processing if so.
265
				if (Configure::read('Queue.workerMaxRuntime') != 0
266
						&& (time() - $workerStartTime) >= Configure::read('Queue.workerMaxRuntime')
267
				) {
268
					$this->__exit = true;
269
					$this->out(__d('queue',
270
						'Reached runtime of %s seconds (max. %s), terminating.',
271
						(time() - $workerStartTime),
272
						Configure::read('Queue.workerMaxRuntime')
273
					));
274
				}
275
276
				if (rand(0, 100) > (100 - Configure::read('Queue.gcprop'))) {
277
					$this->out(__d('queue', 'Performing old job cleanup.'));
278
					$this->QueuedTask->cleanOldJobs($this->_getTaskConf());
279
				}
280
			}
281
		}
282
	}
283
284
/**
285
 * Triggers manual job cleanup of completed jobs.
286
 *
287
 * @return void
288
 */
289
	public function clean() {
290
		$this->out(__d('queue', 'Deleting old completed jobs, that have had cleanup timeout.'));
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

290
		$this->out(/** @scrutinizer ignore-call */ __d('queue', 'Deleting old completed jobs, that have had cleanup timeout.'));
Loading history...
291
		$this->QueuedTask->cleanOldJobs($this->_getTaskConf());
292
	}
293
294
/**
295
 * Triggers manual job cleanup of failed jobs.
296
 *
297
 * @return void
298
 */
299
	public function clean_failed() {
300
		$this->out(__d('queue', 'Deleting failed Jobs, that have had maximum worker retries.'));
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

300
		$this->out(/** @scrutinizer ignore-call */ __d('queue', 'Deleting failed Jobs, that have had maximum worker retries.'));
Loading history...
301
		$this->QueuedTask->cleanFailedJobs($this->_getTaskConf());
302
	}
303
304
/**
305
 * Displays some statistics about finished Jobs.
306
 *
307
 * @return void
308
 */
309
	public function stats() {
310
		$this->hr();
311
		$this->out(__d('queue', 'Jobs currenty in the queue:'));
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

311
		$this->out(/** @scrutinizer ignore-call */ __d('queue', 'Jobs currenty in the queue:'));
Loading history...
312
		$this->hr();
313
314
		$types = $this->QueuedTask->getTypes();
315
		foreach ($types as $type) {
316
			$this->out(sprintf('- %s: %s', $type, $this->QueuedTask->getLength($type)));
317
		}
318
		$this->out();
319
320
		$this->hr();
321
		$this->out(__d('queue', 'Total unfinished jobs: %s', $this->QueuedTask->getLength()));
322
		$this->hr();
323
		$this->out();
324
325
		$this->hr();
326
		$this->out(__d('queue', 'Finished job statistics:'));
327
		$this->hr();
328
329
		$data = $this->QueuedTask->getStats();
330
		foreach ($data as $item) {
331
			$this->out(sprintf('- %s: ', $item['QueuedTask']['task']));
332
			$this->out(sprintf('  - %s', __d('queue', 'Finished jobs in database: %s', $item[0]['num'])));
333
			$this->out(sprintf('  - %s', __d('queue', 'Average job existence: %ss', $item[0]['alltime'])));
334
			$this->out(sprintf('  - %s', __d('queue', 'Average execution delay: %ss', $item[0]['fetchdelay'])));
335
			$this->out(sprintf('  - %s', __d('queue', 'Average execution time: %ss', $item[0]['runtime'])));
336
			$this->out();
337
		}
338
	}
339
340
/**
341
 * Returns a list of available queue tasks and their individual configurations.
342
 *
343
 * @return array A list of available queue tasks and their individual configurations
344
 */
345
	protected function _getTaskConf() {
346
		if (!is_array($this->_taskConf)) {
0 ignored issues
show
introduced by
The condition is_array($this->_taskConf) is always true.
Loading history...
347
			$this->_taskConf = [];
348
			foreach ($this->tasks as $task) {
349
				list($pluginName, $taskName) = pluginSplit($task);
0 ignored issues
show
Bug introduced by
The function pluginSplit was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

349
				list($pluginName, $taskName) = /** @scrutinizer ignore-call */ pluginSplit($task);
Loading history...
350
351
				$this->_taskConf[$taskName]['name'] = substr($taskName, 5);
352
				$this->_taskConf[$taskName]['plugin'] = $pluginName;
353
354
				if (property_exists($this->{$taskName}, 'timeout')) {
355
					$this->_taskConf[$taskName]['timeout'] = $this->{$taskName}->timeout;
356
				} else {
357
					$this->_taskConf[$taskName]['timeout'] = Configure::read('Queue.defaultWorkerTimeout');
358
				}
359
				if (property_exists($this->{$taskName}, 'retries')) {
360
					$this->_taskConf[$taskName]['retries'] = $this->{$taskName}->retries;
361
				} else {
362
					$this->_taskConf[$taskName]['retries'] = Configure::read('Queue.defaultWorkerRetries');
363
				}
364
				if (property_exists($this->{$taskName}, 'cleanupTimeout')) {
365
					$this->_taskConf[$taskName]['cleanupTimeout'] = $this->{$taskName}->cleanupTimeout;
366
				} else {
367
					$this->_taskConf[$taskName]['cleanupTimeout'] = Configure::read('Queue.cleanupTimeout');
368
				}
369
			}
370
		}
371
372
		return $this->_taskConf;
373
	}
374
375
/**
376
 * Signal handler (for SIGTERM and SIGINT signal)
377
 *
378
 * @param int $signalNumber A signal number
379
 * @return void
380
 */
381
	public function signalHandler($signalNumber) {
382
		switch($signalNumber) {
383
			case SIGTERM:
384
				$this->out(__d('queue', 'Caught %s signal, exiting.', sprintf('SIGTERM (%d)', SIGTERM)));
0 ignored issues
show
Bug introduced by
The function __d was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

384
				$this->out(/** @scrutinizer ignore-call */ __d('queue', 'Caught %s signal, exiting.', sprintf('SIGTERM (%d)', SIGTERM)));
Loading history...
385
386
				$this->__exit = true;
387
				break;
388
			case SIGINT:
389
				$this->out(__d('queue', 'Caught %s signal, exiting.', sprintf('SIGINT (%d)', SIGINT)));
390
391
				$this->__exit = true;
392
				break;
393
		}
394
	}
395
396
/**
397
 * Converts string to array
398
 *
399
 * @param string|null $param String to convert
400
 * @return array
401
 */
402
	protected function _stringToArray(string $param = null) : array {
403
		if (!$param) {
404
			return [];
405
		}
406
407
		$array = CakeText::tokenize($param);
0 ignored issues
show
Bug introduced by
The type CakeText was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
408
		if (is_string($array)) {
409
			return [
410
				$array
411
			];
412
		}
413
414
		return array_filter($array);
415
	}
416
417
}
418