Completed
Pull Request — 2.9 (#116)
by
unknown
01:57
created

QueuedJobService::restartStalledJob()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 21
nc 2
nop 1
1
<?php
2
3
/**
4
 * A service that can be used for starting, stopping and listing queued jobs.
5
 *
6
 * When a job is first added, it is initialised, its job type determined, then persisted to the database
7
 *
8
 * When the queues are scanned, a job is reloaded and processed. Ignoring the persistence and reloading, it looks
9
 * something like
10
 *
11
 * job->getJobType();
12
 * job->getJobData();
13
 * data->write();
14
 * job->setup();
15
 * while !job->isComplete
16
 *	job->process();
17
 *	job->getJobData();
18
 *  data->write();
19
 *
20
 *
21
 * @author Marcus Nyeholt <[email protected]>
22
 * @license BSD http://silverstripe.org/bsd-license/
23
 */
24
class QueuedJobService {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
25
	/**
26
	 * @var int
27
	 */
28
	private static $stall_threshold = 3;
0 ignored issues
show
Unused Code introduced by
The property $stall_threshold is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
30
	/**
31
	 * How much ram will we allow before pausing and releasing the memory?
32
	 *
33
	 * For instance, set to 134217728 (128MB) to pause this process if used memory exceeds
34
	 * this value. This needs to be set to a value lower than the php_ini max_memory as
35
	 * the system will otherwise crash before shutdown can be handled gracefully.
36
	 *
37
	 * @var int
38
	 * @config
39
	 */
40
	private static $memory_limit = 134217728;
0 ignored issues
show
Unused Code introduced by
The property $memory_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
41
42
	/**
43
	 * Optional time limit (in seconds) to run the service before restarting to release resources.
44
	 *
45
	 * Defaults to no limit.
46
	 *
47
	 * @var int
48
	 * @config
49
	 */
50
	private static $time_limit = 0;
0 ignored issues
show
Unused Code introduced by
The property $time_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
51
52
	/**
53
	 * Timestamp (in seconds) when the queue was started
54
	 *
55
	 * @var int
56
	 */
57
	protected $startedAt = 0;
58
59
	/**
60
	 * Should "immediate" jobs be managed using the shutdown function?
61
	 *
62
	 * It is recommended you set up an inotify watch and use that for
63
	 * triggering immediate jobs. See the wiki for more information
64
	 *
65
	 * @var boolean
66
	 */
67
	private static $use_shutdown_function = true;
0 ignored issues
show
Unused Code introduced by
The property $use_shutdown_function is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
68
69
	/**
70
	 * The location for immediate jobs to be stored in
71
	 *
72
	 * @var string
73
	 */
74
	private static $cache_dir = 'queuedjobs';
0 ignored issues
show
Unused Code introduced by
The property $cache_dir is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
75
76
	/**
77
	 * @var DefaultQueueHandler
78
	 */
79
	public $queueHandler;
80
81
	/**
82
	 *
83
	 * @var TaskRunnerEngine
84
	 */
85
	public $queueRunner;
86
87
	/**
88
	 * Config controlled list of default/required jobs
89
	 * @var Array
90
	 */
91
	public $defaultJobs;
92
93
	/**
94
	 * Register our shutdown handler
95
	 */
96
	public function __construct() {
97
		// bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
98
		if (Config::inst()->get(__CLASS__, 'use_shutdown_function') && Director::is_cli()) {
99
			if (class_exists('PHPUnit_Framework_TestCase') && SapphireTest::is_running_test()) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
100
				// do NOTHING
101
			} else {
102
				register_shutdown_function(array($this, 'onShutdown'));
103
			}
104
105
		}
106
		if (Config::inst()->get('Email', 'queued_job_admin_email') == '') {
107
			Config::inst()->update('Email', 'queued_job_admin_email', Config::inst()->get('Email', 'admin_email'));
108
		}
109
	}
110
111
	/**
112
	 * Adds a job to the queue to be started
113
	 *
114
	 * Relevant data about the job will be persisted using a QueuedJobDescriptor
115
	 *
116
	 * @param QueuedJob $job
117
	 *			The job to start.
118
	 * @param $startAfter
119
	 *			The date (in Y-m-d H:i:s format) to start execution after
120
	 * @param int $userId
121
	 *			The ID of a user to execute the job as. Defaults to the current user
122
	 * @return int
123
	 */
124
	public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null) {
125
126
		$signature = $job->getSignature();
127
128
		// see if we already have this job in a queue
129
		$filter = array(
130
			'Signature' => $signature,
131
			'JobStatus' => array(
132
				QueuedJob::STATUS_NEW,
133
				QueuedJob::STATUS_INIT
134
			)
135
		);
136
137
		$existing = DataList::create('QueuedJobDescriptor')->filter($filter)->first();
138
139
		if ($existing && $existing->ID) {
140
			return $existing->ID;
141
		}
142
143
		$jobDescriptor = new QueuedJobDescriptor();
144
		$jobDescriptor->JobTitle = $job->getTitle();
145
		$jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
146
		$jobDescriptor->Signature = $signature;
147
		$jobDescriptor->Implementation = get_class($job);
148
		$jobDescriptor->StartAfter = $startAfter;
149
150
		$jobDescriptor->RunAsID = $userId ? $userId : Member::currentUserID();
0 ignored issues
show
Documentation introduced by
The property RunAsID does not exist on object<QueuedJobDescriptor>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
151
152
		// copy data
153
		$this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<QueuedJobDescriptor>, but the function expects a object<JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
154
155
		$jobDescriptor->write();
156
157
		$this->startJob($jobDescriptor, $startAfter);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<QueuedJobDescriptor>, but the function expects a object<JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
158
159
		return $jobDescriptor->ID;
160
	}
161
162
	/**
163
	 * Start a job (or however the queue handler determines it should be started)
164
	 *
165
	 * @param JobDescriptor $jobDescriptor
166
	 * @param date $startAfter
167
	 */
168
	public function startJob($jobDescriptor, $startAfter = null) {
169
		if ($startAfter && strtotime($startAfter) > time()) {
170
			$this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
171
		} else {
172
			// immediately start it on the queue, however that works
173
			$this->queueHandler->startJobOnQueue($jobDescriptor);
174
		}
175
	}
176
177
	/**
178
	 * Copies data from a job into a descriptor for persisting
179
	 *
180
	 * @param QueuedJob $job
181
	 * @param JobDescriptor $jobDescriptor
182
	 */
183
	protected function copyJobToDescriptor($job, $jobDescriptor) {
184
		$data = $job->getJobData();
185
186
		$jobDescriptor->TotalSteps = $data->totalSteps;
187
		$jobDescriptor->StepsProcessed = $data->currentStep;
188
		if ($data->isComplete) {
189
			$jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
190
			$jobDescriptor->JobFinished = date('Y-m-d H:i:s');
191
		}
192
193
		$jobDescriptor->SavedJobData = serialize($data->jobData);
194
		$jobDescriptor->SavedJobMessages = serialize($data->messages);
195
	}
196
197
	/**
198
	 * @param QueuedJobDescriptor $jobDescriptor
199
	 * @param QueuedJob $job
200
	 */
201
	protected function copyDescriptorToJob($jobDescriptor, $job) {
202
		$jobData = null;
0 ignored issues
show
Unused Code introduced by
$jobData is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
203
		$messages = null;
0 ignored issues
show
Unused Code introduced by
$messages is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
204
205
		// switching to php's serialize methods... not sure why this wasn't done from the start!
206
		$jobData = @unserialize($jobDescriptor->SavedJobData);
207
		$messages = @unserialize($jobDescriptor->SavedJobMessages);
208
209
		if (!$jobData) {
210
			// SS's convert:: function doesn't do this detection for us!!
211
			if (function_exists('json_decode')) {
212
				$jobData = json_decode($jobDescriptor->SavedJobData);
213
				$messages = json_decode($jobDescriptor->SavedJobMessages);
214
			} else {
215
				$jobData = Convert::json2obj($jobDescriptor->SavedJobData);
216
				$messages = Convert::json2obj($jobDescriptor->SavedJobMessages);
217
			}
218
		}
219
220
		$job->setJobData(
221
			$jobDescriptor->TotalSteps,
222
			$jobDescriptor->StepsProcessed,
223
			$jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
224
			$jobData,
225
			$messages
226
		);
227
	}
228
229
	/**
230
	 * Check the current job queues and see if any of the jobs currently in there should be started. If so,
231
	 * return the next job that should be executed
232
	 *
233
	 * @param string $type Job type
234
	 * @return QueuedJobDescriptor
235
	 */
236
	public function getNextPendingJob($type = null) {
237
		// Filter jobs by type
238
		$type = $type ?: QueuedJob::QUEUED;
239
		$list = QueuedJobDescriptor::get()
240
			->filter('JobType', $type)
241
			->sort('ID', 'ASC');
242
243
		// see if there's any blocked jobs that need to be resumed
244
		$waitingJob = $list
245
			->filter('JobStatus', QueuedJob::STATUS_WAIT)
246
			->first();
247
		if ($waitingJob) {
248
			return $waitingJob;
249
		}
250
251
		// If there's an existing job either running or pending, the lets just return false to indicate
252
		// that we're still executing
253
		$runningJob = $list
254
			->filter('JobStatus', array(QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN))
255
			->first();
256
		if ($runningJob) {
257
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by QueuedJobService::getNextPendingJob of type QueuedJobDescriptor.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
258
		}
259
260
		// Otherwise, lets find any 'new' jobs that are waiting to execute
261
		$newJob = $list
262
			->filter('JobStatus', QueuedJob::STATUS_NEW)
263
			->where(sprintf(
264
				'"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
265
				SS_DateTime::now()->getValue()
266
			))
267
			->first();
268
269
		return $newJob;
270
	}
271
272
	/**
273
	 * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
274
	 * between each run. If it's not, then we need to flag it as paused due to an error.
275
	 *
276
	 * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
277
	 * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
278
	 * fix them
279
     *
280
     * @param int $queue The queue to check against
281
	 */
282
	public function checkJobHealth($queue = null) {
283
        $queue = $queue ?: QueuedJob::QUEUED;
284
		// Select all jobs currently marked as running
285
		$runningJobs = QueuedJobDescriptor::get()
286
			->filter(array(
287
				'JobStatus' => array(
288
					QueuedJob::STATUS_RUN,
289
					QueuedJob::STATUS_INIT,
290
				),
291
                'JobType' => $queue,
292
			));
293
294
		// If no steps have been processed since the last run, consider it a broken job
295
		// Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
296
		$stalledJobs = $runningJobs
297
			->filter('LastProcessedCount:GreaterThanOrEqual', 0)
298
			->where('"StepsProcessed" = "LastProcessedCount"');
299
		foreach ($stalledJobs as $stalledJob) {
300
			$this->restartStalledJob($stalledJob);
301
		}
302
303
		// now, find those that need to be marked before the next check
304
		// foreach job, mark it as having been incremented
305
		foreach ($runningJobs as $job) {
306
			$job->LastProcessedCount = $job->StepsProcessed;
307
			$job->write();
308
		}
309
310
		// finally, find the list of broken jobs and send an email if there's some found
311
		$brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
312 View Code Duplication
		if ($brokenJobs && $brokenJobs->count()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
313
			SS_Log::log(array(
314
				'errno' => 0,
315
				'errstr' => 'Broken jobs were found in the job queue',
316
				'errfile' => __FILE__,
317
				'errline' => __LINE__,
318
				'errcontext' => array()
319
			), SS_Log::ERR);
320
		}
321
	}
322
323
	/**
324
	 * Checks through all the scheduled jobs that are expected to exist
325
	 */
326
	public function checkdefaultJobs($queue = null) {
327
		$queue = $queue ?: QueuedJob::QUEUED;
328
		if (count($this->defaultJobs)) {
329
330
			$activeJobs = QueuedJobDescriptor::get()->filter(array(
331
					'JobStatus' => array(
332
					QueuedJob::STATUS_NEW,
333
					QueuedJob::STATUS_INIT,
334
					QueuedJob::STATUS_RUN,
335
					QueuedJob::STATUS_WAIT,
336
				),
337
				'JobType' => $queue
338
			));
339
340
			foreach ($this->defaultJobs as $title => $jobConfig) {
341
				if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
342
					SS_Log::log("Default Job config: $title incorrectly set up. Please check the readme for examples", SS_Log::ERR);
343
					continue;
344
				}
345
346
				$job = $activeJobs->filter(array_merge(
347
					array('Implementation' => $jobConfig['type']), $jobConfig['filter']
348
				));
349
350
				if (!$job->count()) {
351
					Email::create()
352
						->setTo(isset($jobConfig['email']) ? $jobConfig['email'] : Email::config()->admin_email)
353
						->setSubject('Default Job "' . $title . '" missing')
354
						->populateTemplate(array('Title' => $title, 'Site' => Director::absoluteBaseURL()))
355
						->populateTemplate($jobConfig)
356
						->setTemplate('QueuedJobsDefaultJob')
357
						->send();
358
359
					if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
360
						if (!isset($jobConfig['construct']) || !isset($jobConfig['startDateFormat']) || !isset($jobConfig['startTimeString'])) {
361
							SS_Log::log("Default Job config: $title incorrectly set up. Please check the readme for examples", SS_Log::ERR);
362
							continue;
363
						}
364
						singleton('QueuedJobService')->queueJob(
365
							Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
366
							date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
367
						);
368
					}
369
				}
370
			}
371
		}
372
	}
373
374
	/**
375
	 * Attempt to restart a stalled job
376
	 *
377
	 * @param QueuedJobDescriptor $stalledJob
378
	 * @return bool True if the job was successfully restarted
379
	 */
380
	protected function restartStalledJob($stalledJob) {
381
		if ($stalledJob->ResumeCounts < Config::inst()->get(__CLASS__, 'stall_threshold')) {
382
			$stalledJob->restart();
383
			$message = sprintf(
384
				_t(
385
					'QueuedJobs.STALLED_JOB_MSG',
386
					'A job named %s appears to have stalled. It will be stopped and restarted, please login to make sure it has continued'
387
				),
388
				$stalledJob->JobTitle
389
			);
390
		} else {
391
			$stalledJob->pause();
392
			$message = sprintf(
393
				_t(
394
					'QueuedJobs.STALLED_JOB_MSG',
395
					'A job named %s appears to have stalled. It has been paused, please login to check it'
396
				),
397
				$stalledJob->JobTitle
398
			);
399
		}
400
401
		singleton('QJUtils')->log($message);
402
		$from = Config::inst()->get('Email', 'admin_email');
403
		$to = Config::inst()->get('Email', 'queued_job_admin_email');
404
		$subject = _t('QueuedJobs.STALLED_JOB', 'Stalled job');
405
		$mail = new Email($from, $to, $subject, $message);
406
		$mail->send();
407
	}
408
409
	/**
410
	 * Prepares the given jobDescriptor for execution. Returns the job that
411
	 * will actually be run in a state ready for executing.
412
	 *
413
	 * Note that this is called each time a job is picked up to be executed from the cron
414
	 * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
415
	 * so your job MUST detect that and act accordingly.
416
	 *
417
	 * @param QueuedJobDescriptor $jobDescriptor
418
	 *			The Job descriptor of a job to prepare for execution
419
	 *
420
	 * @return QueuedJob|boolean
421
	 */
422
	protected function initialiseJob(QueuedJobDescriptor $jobDescriptor) {
423
		// create the job class
424
		$impl = $jobDescriptor->Implementation;
425
		$job = Object::create($impl);
426
		/* @var $job QueuedJob */
427
		if (!$job) {
428
			throw new Exception("Implementation $impl no longer exists");
429
		}
430
431
		$jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
432
		$jobDescriptor->write();
433
434
		// make sure the data is there
435
		$this->copyDescriptorToJob($jobDescriptor, $job);
436
437
		// see if it needs 'setup' or 'restart' called
438
		if ($jobDescriptor->StepsProcessed <= 0) {
439
			$job->setup();
440
		} else {
441
			$job->prepareForRestart();
442
		}
443
444
		// make sure the descriptor is up to date with anything changed
445
		$this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<QueuedJobDescriptor>, but the function expects a object<JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
446
		$jobDescriptor->write();
447
448
		return $job;
449
	}
450
451
	/**
452
	 * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
453
	 * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
454
	 * This may still have a race condition, but this should minimise the possibility.
455
	 * Side effect is the job status will be changed to "Initialised".
456
	 *
457
	 * Assumption is the job has a status of "Queued" or "Wait".
458
	 *
459
	 * @param QueuedJobDescriptor $jobDescriptor
460
	 * @return boolean
461
	 */
462
	protected function grabMutex(QueuedJobDescriptor $jobDescriptor) {
463
		// write the status and determine if any rows were affected, for protection against a
464
		// potential race condition where two or more processes init the same job at once.
465
		// This deliberately does not use write() as that would always update LastEdited
466
		// and thus the row would always be affected.
467
		try {
468
			DB::query(sprintf(
469
				'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s',
470
				QueuedJob::STATUS_INIT,
471
				$jobDescriptor->ID
472
			));
473
		} catch(Exception $e) {
474
			return false;
475
		}
476
477
		if(DB::getConn()->affectedRows() === 0 && $jobDescriptor->JobStatus !== QueuedJob::STATUS_INIT) {
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
478
			return false;
479
		}
480
481
		return true;
482
	}
483
484
	/**
485
	 * Start the actual execution of a job.
486
	 * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
487
	 *
488
	 * This method will continue executing until the job says it's completed
489
	 *
490
	 * @param int $jobId
491
	 *			The ID of the job to start executing
492
	 * @return boolean
493
	 */
494
	public function runJob($jobId) {
0 ignored issues
show
Coding Style introduced by
runJob uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
495
		// first retrieve the descriptor
496
		$jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId);
497
		if (!$jobDescriptor) {
498
			throw new Exception("$jobId is invalid");
499
		}
500
501
		// now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
502
		// we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
503
		// want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
504
		// as the currently logged in user and hope for the best
505
506
		// We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
507
		// this point of execution in some circumstances
508
		$originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
509
		$originalUser = $originalUserID ? DataObject::get_by_id('Member', $originalUserID) : null;
510
		$runAsUser = null;
511
512
		if (Director::is_cli() || !$originalUser || Permission::checkMember($originalUser, 'ADMIN')) {
513
			$runAsUser = $jobDescriptor->RunAs();
514
			if ($runAsUser && $runAsUser->exists()) {
515
				// the job runner outputs content way early in the piece, meaning there'll be cookie errors
516
				// if we try and do a normal login, and we only want it temporarily...
517
				if (Controller::has_curr()) {
518
					Session::set('loggedInAs', $runAsUser->ID);
519
				} else {
520
					$_SESSION['loggedInAs'] = $runAsUser->ID;
521
				}
522
523
				// this is an explicit coupling brought about by SS not having
524
				// a nice way of mocking a user, as it requires session
525
				// nastiness
526
				if (class_exists('SecurityContext')) {
527
					singleton('SecurityContext')->setMember($runAsUser);
528
				}
529
			}
530
		}
531
532
		// set up a custom error handler for this processing
533
		$errorHandler = new JobErrorHandler();
534
535
		$job = null;
536
537
		$broken = false;
538
539
		// Push a config context onto the stack for the duration of this job run.
540
		Config::nest();
541
542
		if($this->grabMutex($jobDescriptor)) {
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<DataObject> is not a sub-type of object<QueuedJobDescriptor>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
543
			try {
544
				$job = $this->initialiseJob($jobDescriptor);
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<DataObject> is not a sub-type of object<QueuedJobDescriptor>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
545
546
				// get the job ready to begin.
547
				if (!$jobDescriptor->JobStarted) {
548
					$jobDescriptor->JobStarted = date('Y-m-d H:i:s');
549
				} else {
550
					$jobDescriptor->JobRestarted = date('Y-m-d H:i:s');
551
				}
552
553
				$jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
554
				$jobDescriptor->write();
555
556
				$lastStepProcessed = 0;
557
				// have we stalled at all?
558
				$stallCount = 0;
559
560
				if ($job->SubsiteID && class_exists('Subsite')) {
561
					Subsite::changeSubsite($job->SubsiteID);
562
563
					// lets set the base URL as far as Director is concerned so that our URLs are correct
564
					$subsite = DataObject::get_by_id('Subsite', $job->SubsiteID);
565
					if ($subsite && $subsite->exists()) {
566
						$domain = $subsite->domain();
567
						$base = rtrim(Director::protocol() . $domain, '/') . '/';
568
569
						Config::inst()->update('Director', 'alternate_base_url', $base);
570
					}
571
				}
572
573
				// while not finished
574
				while (!$job->jobFinished() && !$broken) {
575
					// see that we haven't been set to 'paused' or otherwise by another process
576
					$jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId);
577
					if (!$jobDescriptor || !$jobDescriptor->exists()) {
578
						$broken = true;
579
						SS_Log::log(array(
580
							'errno' => 0,
581
							'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
582
							'errfile' => __FILE__,
583
							'errline' => __LINE__,
584
							'errcontext' => array()
585
						), SS_Log::ERR);
586
						break;
587
					}
588
					if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
589
						// we've been paused by something, so we'll just exit
590
						$job->addMessage(sprintf(_t('QueuedJobs.JOB_PAUSED', "Job paused at %s"), date('Y-m-d H:i:s')));
591
						$broken = true;
592
					}
593
594
					if (!$broken) {
595
						try {
596
							$job->process();
597
						} catch (Exception $e) {
598
							// okay, we'll just catch this exception for now
599
							$job->addMessage(sprintf(_t('QueuedJobs.JOB_EXCEPT', 'Job caused exception %s in %s at line %s'), $e->getMessage(), $e->getFile(), $e->getLine()), 'ERROR');
0 ignored issues
show
Unused Code introduced by
The call to QueuedJob::addMessage() has too many arguments starting with 'ERROR'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
600
							SS_Log::log($e, SS_Log::ERR);
601
							$jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
602
						}
603
604
						// now check the job state
605
						$data = $job->getJobData();
606
						if ($data->currentStep == $lastStepProcessed) {
607
							$stallCount++;
608
						}
609
610
						if ($stallCount > Config::inst()->get(__CLASS__, 'stall_threshold')) {
611
							$broken = true;
612
							$job->addMessage(sprintf(_t('QueuedJobs.JOB_STALLED', "Job stalled after %s attempts - please check"), $stallCount), 'ERROR');
0 ignored issues
show
Unused Code introduced by
The call to QueuedJob::addMessage() has too many arguments starting with 'ERROR'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
613
							$jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
614
						}
615
616
						// now we'll be good and check our memory usage. If it is too high, we'll set the job to
617
						// a 'Waiting' state, and let the next processing run pick up the job.
618
						if ($this->isMemoryTooHigh()) {
619
							$job->addMessage(sprintf(
620
								_t('QueuedJobs.MEMORY_RELEASE', 'Job releasing memory and waiting (%s used)'),
621
								$this->humanReadable($this->getMemoryUsage())
622
							));
623
624
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
625
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
626
                            }
627
628
							$broken = true;
629
						}
630
631
						// Also check if we are running too long
632
						if($this->hasPassedTimeLimit()) {
633
							$job->addMessage(_t(
634
								'QueuedJobs.TIME_LIMIT',
635
								'Queue has passed time limit and will restart before continuing'
636
							));
637
							if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
638
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
639
                            }
640
							$broken = true;
641
						}
642
					}
643
644
					if ($jobDescriptor) {
645
						$this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Bug introduced by
It seems like $job defined by $this->initialiseJob($jobDescriptor) on line 544 can also be of type boolean; however, QueuedJobService::copyJobToDescriptor() does only seem to accept object<QueuedJob>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Documentation introduced by
$jobDescriptor is of type object<DataObject>, but the function expects a object<JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
646
						$jobDescriptor->write();
647 View Code Duplication
					} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
648
						SS_Log::log(array(
649
							'errno' => 0,
650
							'errstr' => 'Job descriptor has been set to null',
651
							'errfile' => __FILE__,
652
							'errline' => __LINE__,
653
							'errcontext' => array()
654
						), SS_Log::WARN);
655
						$broken = true;
656
					}
657
				}
658
659
				// a last final save. The job is complete by now
660
				if ($jobDescriptor) {
661
					$jobDescriptor->write();
662
				}
663
664
				if (!$broken) {
665
					$job->afterComplete();
666
					$jobDescriptor->cleanupJob();
667
				}
668
			} catch (Exception $e) {
669
				// okay, we'll just catch this exception for now
670
				SS_Log::log($e, SS_Log::ERR);
671
				$jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
672
				$jobDescriptor->write();
673
				$broken = true;
674
			}
675
		}
676
677
		$errorHandler->clear();
678
679
		Config::unnest();
680
681
		// okay let's reset our user if we've got an original
682
		if ($runAsUser && $originalUser) {
683
			Session::clear("loggedInAs");
684
			if ($originalUser) {
685
				Session::set("loggedInAs", $originalUser->ID);
686
			}
687
		}
688
689
		return !$broken;
690
	}
691
692
	/**
693
	 * Start timer
694
	 */
695
	protected function markStarted() {
696
		if($this->startedAt) {
697
			$this->startedAt = SS_Datetime::now()->Format('U');
0 ignored issues
show
Documentation Bug introduced by
It seems like \SS_Datetime::now()->Format('U') can also be of type string. However, the property $startedAt is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
698
		}
699
	}
700
701
	/**
702
	 * Is execution time too long?
703
	 *
704
	 * @return bool True if the script has passed the configured time_limit
705
	 */
706
	protected function hasPassedTimeLimit() {
707
		// Ensure a limit exists
708
		$limit = Config::inst()->get(__CLASS__, 'time_limit');
709
		if(!$limit) {
710
			return false;
711
		}
712
713
		// Ensure started date is set
714
		$this->markStarted();
715
716
		// Check duration
717
		$now = SS_Datetime::now()->Format('U');
718
		return $now > $this->startedAt + $limit;
719
	}
720
721
	/**
722
	 * Is memory usage too high?
723
	 *
724
	 * @return bool
725
	 */
726
	protected function isMemoryTooHigh() {
727
		$used = $this->getMemoryUsage();
728
		$limit = $this->getMemoryLimit();
729
		return $limit && ($used > $limit);
730
	}
731
732
	/**
733
	 * Get peak memory usage of this application
734
	 *
735
	 * @return float
736
	 */
737
	protected function getMemoryUsage() {
738
		// Note we use real_usage = false http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
739
		// Also we use the safer peak memory usage
740
		return (float)memory_get_peak_usage(false);
741
	}
742
743
	/**
744
	 * Determines the memory limit (in bytes) for this application
745
	 * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
746
	 *
747
	 * @return float Memory limit in bytes
748
	 */
749
	protected function getMemoryLimit() {
750
		// Limit to smaller of explicit limit or php memory limit
751
		$limit = $this->parseMemory(Config::inst()->get(__CLASS__, 'memory_limit'));
752
		if($limit) {
753
			return $limit;
754
		}
755
756
		// Fallback to php memory limit
757
		$phpLimit = $this->getPHPMemoryLimit();
758
		if($phpLimit) {
759
			return $phpLimit;
760
		}
761
	}
762
763
	/**
764
	 * Calculate the current memory limit of the server
765
	 *
766
	 * @return float
767
	 */
768
	protected function getPHPMemoryLimit() {
769
		return $this->parseMemory(trim(ini_get("memory_limit")));
770
	}
771
772
	/**
773
	 * Convert memory limit string to bytes.
774
	 * Based on implementation in install.php5
775
	 *
776
	 * @param string $memString
777
	 * @return float
778
	 */
779
	protected function parseMemory($memString) {
780
		switch(strtolower(substr($memString, -1))) {
781
			case "b":
782
				return round(substr($memString, 0, -1));
783
			case "k":
784
				return round(substr($memString, 0, -1) * 1024);
785
			case "m":
786
				return round(substr($memString, 0, -1) * 1024 * 1024);
787
			case "g":
788
				return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
789
			default:
790
				return round($memString);
791
		}
792
	}
793
794
	protected function humanReadable($size) {
795
		$filesizename = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
796
		return $size ? round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
797
	}
798
799
800
	/**
801
	 * Gets a list of all the current jobs (or jobs that have recently finished)
802
	 *
803
	 * @param string $type
804
	 *			if we're after a particular job list
805
	 * @param int $includeUpUntil
806
	 *			The number of seconds to include jobs that have just finished, allowing a job list to be built that
807
	 *			includes recently finished jobs
808
	 */
809
	public function getJobList($type = null, $includeUpUntil = 0) {
810
		return DataObject::get('QueuedJobDescriptor', $this->getJobListFilter($type, $includeUpUntil));
811
	}
812
813
	/**
814
	 * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
815
	 *
816
	 * @param string $type
817
	 *			if we're after a particular job list
818
	 * @param int $includeUpUntil
819
	 *			The number of seconds to include jobs that have just finished, allowing a job list to be built that
820
	 *			includes recently finished jobs
821
	 * @return string
822
	 */
823
	public function getJobListFilter($type = null, $includeUpUntil = 0) {
824
		$filter = array('JobStatus <>' => QueuedJob::STATUS_COMPLETE);
825
		if ($includeUpUntil) {
826
			$filter['JobFinished > '] = date('Y-m-d H:i:s', time() - $includeUpUntil);
827
		}
828
829
		$filter = singleton('QJUtils')->dbQuote($filter, ' OR ');
830
831
		if ($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
832
			$filter = singleton('QJUtils')->dbQuote(array('JobType =' => (string) $type)) . ' AND ('.$filter.')';
833
		}
834
835
		return $filter;
836
	}
837
838
	/**
839
	 * Process the job queue with the current queue runner
840
	 *
841
	 * @param string $queue
842
	 */
843
	public function runQueue($queue) {
844
		$this->checkJobHealth($queue);
845
		$this->checkdefaultJobs($queue);
846
		$this->queueRunner->runQueue($queue);
847
	}
848
849
	/**
850
	 * Process all jobs from a given queue
851
	 *
852
	 * @param string $name The job queue to completely process
853
	 */
854
	public function processJobQueue($name) {
0 ignored issues
show
Coding Style introduced by
processJobQueue uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
855
		// Start timer to measure lifetime
856
		$this->markStarted();
857
858
		// Begin main loop
859
		do {
860
			if (class_exists('Subsite')) {
861
				// clear subsite back to default to prevent any subsite changes from leaking to
862
				// subsequent actions
863
				Subsite::changeSubsite(0);
864
			}
865
			if (Controller::has_curr()) {
866
				Session::clear('loggedInAs');
867
			} else {
868
				unset($_SESSION['loggedInAs']);
869
			}
870
871
			if (class_exists('SecurityContext')) {
872
				singleton('SecurityContext')->setMember(null);
873
			}
874
875
			$job = $this->getNextPendingJob($name);
876
			if ($job) {
877
				$success = $this->runJob($job->ID);
878
				if (!$success) {
879
					// make sure job is null so it doesn't continue the current
880
					// processing loop. Next queue executor can pick up where
881
					// things left off
882
					$job = null;
883
				}
884
			}
885
		} while($job);
886
	}
887
888
	/**
889
	 * When PHP shuts down, we want to process all of the immediate queue items
890
	 *
891
	 * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
892
	 * we ignore paused or stalled jobs.
893
	 */
894
	public function onShutdown() {
895
		$this->processJobQueue(QueuedJob::IMMEDIATE);
896
	}
897
}
898
899
/**
900
 * Class used to handle errors for a single job
901
 */
902
class JobErrorHandler {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
903
	public function __construct() {
904
		set_error_handler(array($this, 'handleError'));
905
	}
906
907
	public function clear() {
908
		restore_error_handler();
909
	}
910
911
	public function handleError($errno, $errstr, $errfile, $errline) {
912
		if (error_reporting()) {
913
			// Don't throw E_DEPRECATED in PHP 5.3+
914
			if (defined('E_DEPRECATED')) {
915
				if ($errno == E_DEPRECATED || $errno = E_USER_DEPRECATED) {
916
					return;
917
				}
918
			}
919
920
			switch ($errno) {
921
				case E_NOTICE:
922
				case E_USER_NOTICE:
923
				case E_STRICT: {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
924
					break;
925
				}
926
				default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
927
					throw new Exception($errstr . " in $errfile at line $errline", $errno);
928
					break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
929
				}
930
			}
931
		}
932
	}
933
}
934