GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 646ea8...4450cc )
by Robbie
13s
created

QueuedJobService::checkDefaultJobs()   C

Complexity

Conditions 13
Paths 14

Size

Total Lines 49
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 49
rs 5.1401
c 0
b 0
f 0
cc 13
eloc 37
nc 14
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Symbiote\QueuedJobs\Services;
4
5
use Exception;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Control\Email\Email;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Config\Configurable;
12
use SilverStripe\Core\Convert;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\Dev\SapphireTest;
15
use SilverStripe\ORM\DataList;
16
use SilverStripe\ORM\DataObject;
17
use SilverStripe\ORM\DB;
18
use SilverStripe\ORM\FieldType\DBDatetime;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\Permission;
21
use SilverStripe\Security\Security;
22
use Psr\Log\LoggerInterface;
23
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
24
use Symbiote\QueuedJobs\QJUtils;
25
26
/**
27
 * A service that can be used for starting, stopping and listing queued jobs.
28
 *
29
 * When a job is first added, it is initialised, its job type determined, then persisted to the database
30
 *
31
 * When the queues are scanned, a job is reloaded and processed. Ignoring the persistence and reloading, it looks
32
 * something like
33
 *
34
 * job->getJobType();
35
 * job->getJobData();
36
 * data->write();
37
 * job->setup();
38
 * while !job->isComplete
39
 *  job->process();
40
 *  job->getJobData();
41
 *  data->write();
42
 *
43
 *
44
 * @author Marcus Nyeholt <[email protected]>
45
 * @license BSD http://silverstripe.org/bsd-license/
46
 */
47
class QueuedJobService
48
{
49
    use Configurable;
50
51
    /**
52
     * @config
53
     * @var int
54
     */
55
    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...
56
57
    /**
58
     * How much ram will we allow before pausing and releasing the memory?
59
     *
60
     * For instance, set to 268435456 (256MB) to pause this process if used memory exceeds
61
     * this value. This needs to be set to a value lower than the php_ini max_memory as
62
     * the system will otherwise crash before shutdown can be handled gracefully.
63
     *
64
     * This was increased to 256MB for SilverStripe 4.x as framework uses more memory than 3.x
65
     *
66
     * @var int
67
     * @config
68
     */
69
    private static $memory_limit = 268435456;
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...
70
71
    /**
72
     * Optional time limit (in seconds) to run the service before restarting to release resources.
73
     *
74
     * Defaults to no limit.
75
     *
76
     * @var int
77
     * @config
78
     */
79
    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...
80
81
    /**
82
     * Timestamp (in seconds) when the queue was started
83
     *
84
     * @var int
85
     */
86
    protected $startedAt = 0;
87
88
    /**
89
     * Should "immediate" jobs be managed using the shutdown function?
90
     *
91
     * It is recommended you set up an inotify watch and use that for
92
     * triggering immediate jobs. See the wiki for more information
93
     *
94
     * @var boolean
95
     * @config
96
     */
97
    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...
98
99
    /**
100
     * The location for immediate jobs to be stored in
101
     *
102
     * @var string
103
     * @config
104
     */
105
    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...
106
107
    /**
108
     * @var DefaultQueueHandler
109
     */
110
    public $queueHandler;
111
112
    /**
113
     *
114
     * @var TaskRunnerEngine
115
     */
116
    public $queueRunner;
117
118
    /**
119
     * Config controlled list of default/required jobs
120
     * @var array
121
     */
122
    public $defaultJobs = [];
123
124
    /**
125
     * Register our shutdown handler
126
     */
127
    public function __construct()
128
    {
129
        // bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
130
        if (static::config()->get('use_shutdown_function') && Director::is_cli()) {
131
            register_shutdown_function(array($this, 'onShutdown'));
132
        }
133
        if (Config::inst()->get(Email::class, 'queued_job_admin_email') == '') {
134
            Config::modify()->set(
135
                Email::class,
136
                'queued_job_admin_email',
137
                Config::inst()->get(Email::class, 'admin_email')
138
            );
139
        }
140
    }
141
142
    /**
143
     * Adds a job to the queue to be started
144
     *
145
     * Relevant data about the job will be persisted using a QueuedJobDescriptor
146
     *
147
     * @param QueuedJob $job
148
     *          The job to start.
149
     * @param $startAfter
150
     *          The date (in Y-m-d H:i:s format) to start execution after
151
     * @param int $userId
152
     *          The ID of a user to execute the job as. Defaults to the current user
153
     * @return int
154
     */
155
    public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
156
    {
157
        $signature = $job->getSignature();
158
159
        // see if we already have this job in a queue
160
        $filter = array(
161
            'Signature' => $signature,
162
            'JobStatus' => array(
163
                QueuedJob::STATUS_NEW,
164
                QueuedJob::STATUS_INIT
165
            )
166
        );
167
168
        $existing = DataList::create(QueuedJobDescriptor::class)
169
            ->filter($filter)
170
            ->first();
171
172
        if ($existing && $existing->ID) {
173
            return $existing->ID;
174
        }
175
176
        $jobDescriptor = new QueuedJobDescriptor();
177
        $jobDescriptor->JobTitle = $job->getTitle();
178
        $jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
179
        $jobDescriptor->Signature = $signature;
180
        $jobDescriptor->Implementation = get_class($job);
181
        $jobDescriptor->StartAfter = $startAfter;
182
183
        $jobDescriptor->RunAsID = $userId ? $userId : Security::getCurrentUser()->ID;
0 ignored issues
show
Documentation introduced by
The property RunAsID does not exist on object<Symbiote\QueuedJo...ts\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...
184
185
        // copy data
186
        $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<Symbiote\QueuedJo...ts\QueuedJobDescriptor>, but the function expects a object<Symbiote\QueuedJo...Services\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...
187
188
        $jobDescriptor->write();
189
190
        $this->startJob($jobDescriptor, $startAfter);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<Symbiote\QueuedJo...ts\QueuedJobDescriptor>, but the function expects a object<Symbiote\QueuedJo...Services\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...
191
192
        return $jobDescriptor->ID;
193
    }
194
195
    /**
196
     * Start a job (or however the queue handler determines it should be started)
197
     *
198
     * @param JobDescriptor $jobDescriptor
199
     * @param date $startAfter
200
     */
201
    public function startJob($jobDescriptor, $startAfter = null)
202
    {
203
        if ($startAfter && strtotime($startAfter) > time()) {
204
            $this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
205
        } else {
206
            // immediately start it on the queue, however that works
207
            $this->queueHandler->startJobOnQueue($jobDescriptor);
208
        }
209
    }
210
211
    /**
212
     * Copies data from a job into a descriptor for persisting
213
     *
214
     * @param QueuedJob $job
215
     * @param JobDescriptor $jobDescriptor
216
     */
217
    protected function copyJobToDescriptor($job, $jobDescriptor)
218
    {
219
        $data = $job->getJobData();
220
221
        $jobDescriptor->TotalSteps = $data->totalSteps;
222
        $jobDescriptor->StepsProcessed = $data->currentStep;
223
        if ($data->isComplete) {
224
            $jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
225
            $jobDescriptor->JobFinished = date('Y-m-d H:i:s');
226
        }
227
228
        $jobDescriptor->SavedJobData = serialize($data->jobData);
229
        $jobDescriptor->SavedJobMessages = serialize($data->messages);
230
    }
231
232
    /**
233
     * @param QueuedJobDescriptor $jobDescriptor
234
     * @param QueuedJob $job
235
     */
236
    protected function copyDescriptorToJob($jobDescriptor, $job)
237
    {
238
        $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...
239
        $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...
240
241
        // switching to php's serialize methods... not sure why this wasn't done from the start!
242
        $jobData = @unserialize($jobDescriptor->SavedJobData);
243
        $messages = @unserialize($jobDescriptor->SavedJobMessages);
244
245
        if (!$jobData) {
246
            // SS's convert:: function doesn't do this detection for us!!
247
            if (function_exists('json_decode')) {
248
                $jobData = json_decode($jobDescriptor->SavedJobData);
249
                $messages = json_decode($jobDescriptor->SavedJobMessages);
250
            } else {
251
                $jobData = Convert::json2obj($jobDescriptor->SavedJobData);
252
                $messages = Convert::json2obj($jobDescriptor->SavedJobMessages);
253
            }
254
        }
255
256
        $job->setJobData(
257
            $jobDescriptor->TotalSteps,
258
            $jobDescriptor->StepsProcessed,
259
            $jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
260
            $jobData,
261
            $messages
262
        );
263
    }
264
265
    /**
266
     * Check the current job queues and see if any of the jobs currently in there should be started. If so,
267
     * return the next job that should be executed
268
     *
269
     * @param string $type Job type
270
     * @return QueuedJobDescriptor
271
     */
272
    public function getNextPendingJob($type = null)
273
    {
274
        // Filter jobs by type
275
        $type = $type ?: QueuedJob::QUEUED;
276
        $list = QueuedJobDescriptor::get()
277
            ->filter('JobType', $type)
278
            ->sort('ID', 'ASC');
279
280
        // see if there's any blocked jobs that need to be resumed
281
        $waitingJob = $list
282
            ->filter('JobStatus', QueuedJob::STATUS_WAIT)
283
            ->first();
284
        if ($waitingJob) {
285
            return $waitingJob;
286
        }
287
288
        // If there's an existing job either running or pending, the lets just return false to indicate
289
        // that we're still executing
290
        $runningJob = $list
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $runningJob is correct as $list->filter('JobStatus...::STATUS_RUN))->first() (which targets SilverStripe\ORM\DataList::first()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
291
            ->filter('JobStatus', array(QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN))
292
            ->first();
293
        if ($runningJob) {
294
            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 Symbiote\QueuedJobs\Serv...vice::getNextPendingJob of type Symbiote\QueuedJobs\Data...ueuedJobDescriptor|null.

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...
295
        }
296
297
        // Otherwise, lets find any 'new' jobs that are waiting to execute
298
        $newJob = $list
299
            ->filter('JobStatus', QueuedJob::STATUS_NEW)
300
            ->where(sprintf(
301
                '"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
302
                DBDatetime::now()->getValue()
303
            ))
304
            ->first();
305
306
        return $newJob;
307
    }
308
309
    /**
310
     * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
311
     * between each run. If it's not, then we need to flag it as paused due to an error.
312
     *
313
     * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
314
     * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
315
     * fix them
316
     *
317
     * @param int $queue The queue to check against
318
     */
319
    public function checkJobHealth($queue = null)
320
    {
321
        $queue = $queue ?: QueuedJob::QUEUED;
322
        // Select all jobs currently marked as running
323
        $runningJobs = QueuedJobDescriptor::get()
324
            ->filter(array(
325
                'JobStatus' => array(
326
                    QueuedJob::STATUS_RUN,
327
                    QueuedJob::STATUS_INIT,
328
                ),
329
                'JobType' => $queue,
330
            ));
331
332
        // If no steps have been processed since the last run, consider it a broken job
333
        // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
334
        $stalledJobs = $runningJobs
335
            ->filter('LastProcessedCount:GreaterThanOrEqual', 0)
336
            ->where('"StepsProcessed" = "LastProcessedCount"');
337
        foreach ($stalledJobs as $stalledJob) {
338
            $this->restartStalledJob($stalledJob);
339
        }
340
341
        // now, find those that need to be marked before the next check
342
        // foreach job, mark it as having been incremented
343
        foreach ($runningJobs as $job) {
344
            $job->LastProcessedCount = $job->StepsProcessed;
345
            $job->write();
346
        }
347
348
        // finally, find the list of broken jobs and send an email if there's some found
349
        $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
350 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...
351
            $this->getLogger()->error(
352
                print_r(
353
                    array(
354
                        'errno' => 0,
355
                        'errstr' => 'Broken jobs were found in the job queue',
356
                        'errfile' => __FILE__,
357
                        'errline' => __LINE__,
358
                        'errcontext' => array()
359
                    ),
360
                    true
361
                )
362
            );
363
        }
364
    }
365
366
    /**
367
     * Checks through ll the scheduled jobs that are expected to exist
368
     */
369
    public function checkDefaultJobs($queue = null)
370
    {
371
        $queue = $queue ?: QueuedJob::QUEUED;
0 ignored issues
show
Unused Code introduced by
$queue 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...
372
        if (count($this->defaultJobs)) {
373
            $activeJobs = QueuedJobDescriptor::get()->filter(
374
                'JobStatus',
375
                array(
376
                    QueuedJob::STATUS_NEW,
377
                    QueuedJob::STATUS_INIT,
378
                    QueuedJob::STATUS_RUN,
379
                    QueuedJob::STATUS_WAIT,
380
                    QueuedJob::STATUS_PAUSED,
381
                )
382
            );
383
            foreach ($this->defaultJobs as $title => $jobConfig) {
384
                if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
385
                    $this->getLogger()->error("Default Job config: $title incorrectly set up. Please check the readme for examples");
386
                    continue;
387
                }
388
                $job = $activeJobs->filter(array_merge(
389
                    array('Implementation' => $jobConfig['type']),
390
                    $jobConfig['filter']
391
                ));
392
                if (!$job->count()) {
393
                    $this->getLogger()->error("Default Job config: $title was missing from Queue");
394
                    Email::create()
395
                        ->setTo(isset($jobConfig['email']) ? $jobConfig['email'] : Config::inst()->get('Email', 'queued_job_admin_email'))
396
                        ->setFrom(Config::inst()->get('Email', 'queued_job_admin_email'))
397
                        ->setSubject('Default Job "' . $title . '" missing')
398
                        ->setData($jobConfig)
399
                        ->addData('Title', $title)
400
                        ->addData('Site', Director::absoluteBaseURL())
0 ignored issues
show
Security Bug introduced by
It seems like \SilverStripe\Control\Director::absoluteBaseURL() targeting SilverStripe\Control\Director::absoluteBaseURL() can also be of type false; however, SilverStripe\Control\Email\Email::addData() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
401
                        ->setHTMLTemplate('QueuedJobsDefaultJob')
402
                        ->send();
403
                    if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
404
                        if (!array_key_exists('construct', $jobConfig) || !isset($jobConfig['startDateFormat']) || !isset($jobConfig['startTimeString'])) {
405
                            $this->getLogger()->error("Default Job config: $title incorrectly set up. Please check the readme for examples");
406
                            continue;
407
                        }
408
                        singleton('Symbiote\\QueuedJobs\\Services\\QueuedJobService')->queueJob(
409
                            Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
410
                            date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
411
                        );
412
                        $this->getLogger()->error("Default Job config: $title has been re-added to the Queue");
413
                    }
414
                }
415
            }
416
        }
417
    }
418
419
    /**
420
     * Attempt to restart a stalled job
421
     *
422
     * @param QueuedJobDescriptor $stalledJob
423
     * @return bool True if the job was successfully restarted
424
     */
425
    protected function restartStalledJob($stalledJob)
426
    {
427
        if ($stalledJob->ResumeCounts < static::config()->get('stall_threshold')) {
428
            $stalledJob->restart();
429
            $message = _t(
430
                __CLASS__ . '.STALLED_JOB_RESTART_MSG',
431
                'A job named {name} appears to have stalled. It will be stopped and restarted, please login to make sure it has continued',
432
                ['name' => $stalledJob->JobTitle]
433
            );
434
        } else {
435
            $stalledJob->pause();
436
            $message = _t(
437
                __CLASS__ . '.STALLED_JOB_MSG',
438
                'A job named {name} appears to have stalled. It has been paused, please login to check it',
439
                ['name' => $stalledJob->JobTitle]
440
            );
441
        }
442
443
        $this->getLogger()->error($message);
444
        $from = Config::inst()->get(Email::class, 'admin_email');
445
        $to = Config::inst()->get(Email::class, 'queued_job_admin_email');
446
        $subject = _t(__CLASS__ . '.STALLED_JOB', 'Stalled job');
447
        $mail = new Email($from, $to, $subject, $message);
448
        $mail->send();
449
    }
450
451
    /**
452
     * Prepares the given jobDescriptor for execution. Returns the job that
453
     * will actually be run in a state ready for executing.
454
     *
455
     * Note that this is called each time a job is picked up to be executed from the cron
456
     * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
457
     * so your job MUST detect that and act accordingly.
458
     *
459
     * @param QueuedJobDescriptor $jobDescriptor
460
     *          The Job descriptor of a job to prepare for execution
461
     *
462
     * @return QueuedJob|boolean
463
     */
464
    protected function initialiseJob(QueuedJobDescriptor $jobDescriptor)
465
    {
466
        // create the job class
467
        $impl = $jobDescriptor->Implementation;
468
        $job = Injector::inst()->create($impl);
469
        /* @var $job QueuedJob */
470
        if (!$job) {
471
            throw new Exception("Implementation $impl no longer exists");
472
        }
473
474
        $jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
475
        $jobDescriptor->write();
476
477
        // make sure the data is there
478
        $this->copyDescriptorToJob($jobDescriptor, $job);
479
480
        // see if it needs 'setup' or 'restart' called
481
        if ($jobDescriptor->StepsProcessed <= 0) {
482
            $job->setup();
483
        } else {
484
            $job->prepareForRestart();
485
        }
486
487
        // make sure the descriptor is up to date with anything changed
488
        $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<Symbiote\QueuedJo...ts\QueuedJobDescriptor>, but the function expects a object<Symbiote\QueuedJo...Services\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...
489
        $jobDescriptor->write();
490
491
        return $job;
492
    }
493
494
    /**
495
     * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
496
     * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
497
     * This may still have a race condition, but this should minimise the possibility.
498
     * Side effect is the job status will be changed to "Initialised".
499
     *
500
     * Assumption is the job has a status of "Queued" or "Wait".
501
     *
502
     * @param QueuedJobDescriptor $jobDescriptor
503
     * @return boolean
504
     */
505
    protected function grabMutex(QueuedJobDescriptor $jobDescriptor)
506
    {
507
        // write the status and determine if any rows were affected, for protection against a
508
        // potential race condition where two or more processes init the same job at once.
509
        // This deliberately does not use write() as that would always update LastEdited
510
        // and thus the row would always be affected.
511
        try {
512
            DB::query(sprintf(
513
                'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s',
514
                QueuedJob::STATUS_INIT,
515
                $jobDescriptor->ID
516
            ));
517
        } catch (Exception $e) {
518
            return false;
519
        }
520
521
        if (DB::getConn()->affectedRows() === 0 && $jobDescriptor->JobStatus !== QueuedJob::STATUS_INIT) {
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\ORM\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...
522
            return false;
523
        }
524
525
        return true;
526
    }
527
528
    /**
529
     * Start the actual execution of a job.
530
     * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
531
     *
532
     * This method will continue executing until the job says it's completed
533
     *
534
     * @param int $jobId
535
     *          The ID of the job to start executing
536
     * @return boolean
537
     */
538
    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...
539
    {
540
        // first retrieve the descriptor
541
        $jobDescriptor = DataObject::get_by_id(
542
            QueuedJobDescriptor::class,
543
            (int) $jobId
544
        );
545
        if (!$jobDescriptor) {
546
            throw new Exception("$jobId is invalid");
547
        }
548
549
        // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
550
        // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
551
        // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
552
        // as the currently logged in user and hope for the best
553
554
        // We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
555
        // this point of execution in some circumstances
556
        $originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
557
        $originalUser = $originalUserID
558
            ? DataObject::get_by_id(Member::class, $originalUserID)
559
            : null;
560
        $runAsUser = null;
561
562
        if (Director::is_cli() || !$originalUser || Permission::checkMember($originalUser, 'ADMIN')) {
563
            $runAsUser = $jobDescriptor->RunAs();
564
            if ($runAsUser && $runAsUser->exists()) {
565
                // the job runner outputs content way early in the piece, meaning there'll be cookie errors
566
                // if we try and do a normal login, and we only want it temporarily...
567
                if (Controller::has_curr()) {
568
                    Controller::curr()->getRequest()->getSession()->set('loggedInAs', $runAsUser->ID);
569
                } else {
570
                    $_SESSION['loggedInAs'] = $runAsUser->ID;
571
                }
572
573
                // this is an explicit coupling brought about by SS not having
574
                // a nice way of mocking a user, as it requires session
575
                // nastiness
576
                if (class_exists('SecurityContext')) {
577
                    singleton('SecurityContext')->setMember($runAsUser);
578
                }
579
            }
580
        }
581
582
        // set up a custom error handler for this processing
583
        $errorHandler = new JobErrorHandler();
584
585
        $job = null;
586
587
        $broken = false;
588
589
        // Push a config context onto the stack for the duration of this job run.
590
        Config::nest();
591
592
        if ($this->grabMutex($jobDescriptor)) {
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<SilverStripe\ORM\DataObject> is not a sub-type of object<Symbiote\QueuedJo...ts\QueuedJobDescriptor>. It seems like you assume a child class of the class SilverStripe\ORM\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...
593
            try {
594
                $job = $this->initialiseJob($jobDescriptor);
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<SilverStripe\ORM\DataObject> is not a sub-type of object<Symbiote\QueuedJo...ts\QueuedJobDescriptor>. It seems like you assume a child class of the class SilverStripe\ORM\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...
595
596
                // get the job ready to begin.
597
                if (!$jobDescriptor->JobStarted) {
598
                    $jobDescriptor->JobStarted = date('Y-m-d H:i:s');
599
                } else {
600
                    $jobDescriptor->JobRestarted = date('Y-m-d H:i:s');
601
                }
602
603
                // Only write to job as "Running" if 'isComplete' was NOT set to true
604
                // during setup() or prepareForRestart()
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
605
                if (!$job->jobFinished()) {
606
                    $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
607
                    $jobDescriptor->write();
608
                }
609
610
                $lastStepProcessed = 0;
611
                // have we stalled at all?
612
                $stallCount = 0;
613
614
                if ($job->SubsiteID && class_exists('Subsite')) {
615
                    /**
616
                     * @todo Check for 4.x compatibility with Subsites once namespacing is implemented
617
                     */
618
                    \Subsite::changeSubsite($job->SubsiteID);
619
620
                    // lets set the base URL as far as Director is concerned so that our URLs are correct
621
                    $subsite = DataObject::get_by_id('Subsite', $job->SubsiteID);
622
                    if ($subsite && $subsite->exists()) {
623
                        $domain = $subsite->domain();
624
                        $base = rtrim(Director::protocol() . $domain, '/') . '/';
625
626
                        Config::modify()->set(Director::class, 'alternate_base_url', $base);
627
                    }
628
                }
629
630
                // while not finished
631
                while (!$job->jobFinished() && !$broken) {
632
                    // see that we haven't been set to 'paused' or otherwise by another process
633
                    $jobDescriptor = DataObject::get_by_id(
634
                        QueuedJobDescriptor::class,
635
                        (int) $jobId
636
                    );
637
                    if (!$jobDescriptor || !$jobDescriptor->exists()) {
638
                        $broken = true;
639
                        $this->getLogger()->error(
640
                            print_r(
641
                                array(
642
                                    'errno' => 0,
643
                                    'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
644
                                    'errfile' => __FILE__,
645
                                    'errline' => __LINE__,
646
                                    'errcontext' => array()
647
                                ),
648
                                true
649
                            )
650
                        );
651
                        break;
652
                    }
653
                    if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
654
                        // we've been paused by something, so we'll just exit
655
                        $job->addMessage(
656
                            _t(__CLASS__ . '.JOB_PAUSED', 'Job paused at {time}', ['time' => date('Y-m-d H:i:s')])
657
                        );
658
                        $broken = true;
659
                    }
660
661
                    if (!$broken) {
662
                        try {
663
                            $job->process();
664
                        } catch (Exception $e) {
665
                            // okay, we'll just catch this exception for now
666
                            $job->addMessage(
667
                                _t(
668
                                    __CLASS__ . '.JOB_EXCEPT',
669
                                    'Job caused exception {message} in {file} at line {line}',
670
                                    [
671
                                        'message' => $e->getMessage(),
672
                                        'file' => $e->getFile(),
673
                                        'line' => $e->getLine(),
674
                                    ]
675
                                ),
676
                                '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...
677
                            );
678
                            $this->getLogger()->error($e->getMessage());
679
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
680
                        }
681
682
                        // now check the job state
683
                        $data = $job->getJobData();
684
                        if ($data->currentStep == $lastStepProcessed) {
685
                            $stallCount++;
686
                        }
687
688
                        if ($stallCount > static::config()->get('stall_threshold')) {
689
                            $broken = true;
690
                            $job->addMessage(
691
                                _t(
692
                                    __CLASS__ . '.JOB_STALLED',
693
                                    'Job stalled after {attempts} attempts - please check',
694
                                    ['attempts' => $stallCount]
695
                                ),
696
                                '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...
697
                            );
698
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
699
                        }
700
701
                        // now we'll be good and check our memory usage. If it is too high, we'll set the job to
702
                        // a 'Waiting' state, and let the next processing run pick up the job.
703
                        if ($this->isMemoryTooHigh()) {
704
                            $job->addMessage(
705
                                _t(
706
                                    __CLASS__ . '.MEMORY_RELEASE',
707
                                    'Job releasing memory and waiting ({used} used)',
708
                                    ['used' => $this->humanReadable($this->getMemoryUsage())]
709
                                )
710
                            );
711
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
712
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
713
                            }
714
                            $broken = true;
715
                        }
716
717
                        // Also check if we are running too long
718
                        if ($this->hasPassedTimeLimit()) {
719
                            $job->addMessage(_t(
720
                                __CLASS__ . '.TIME_LIMIT',
721
                                'Queue has passed time limit and will restart before continuing'
722
                            ));
723
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
724
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
725
                            }
726
                            $broken = true;
727
                        }
728
                    }
729
730
                    if ($jobDescriptor) {
731
                        $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Bug introduced by
It seems like $job defined by $this->initialiseJob($jobDescriptor) on line 594 can also be of type boolean; however, Symbiote\QueuedJobs\Serv...::copyJobToDescriptor() does only seem to accept object<Symbiote\QueuedJobs\Services\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<SilverStripe\ORM\DataObject>, but the function expects a object<Symbiote\QueuedJo...Services\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...
732
                        $jobDescriptor->write();
733 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...
734
                        $this->getLogger()->error(
735
                            print_r(
736
                                array(
737
                                    'errno' => 0,
738
                                    'errstr' => 'Job descriptor has been set to null',
739
                                    'errfile' => __FILE__,
740
                                    'errline' => __LINE__,
741
                                    'errcontext' => array()
742
                                ),
743
                                true
744
                            )
745
                        );
746
                        $broken = true;
747
                    }
748
                }
749
750
                // a last final save. The job is complete by now
751
                if ($jobDescriptor) {
752
                    $jobDescriptor->write();
753
                }
754
755
                if ($job->jobFinished()) {
756
                    $job->afterComplete();
757
                    $jobDescriptor->cleanupJob();
758
                }
759
            } catch (Exception $e) {
760
                // okay, we'll just catch this exception for now
761
                $this->getLogger()->error($e->getMessage());
762
                $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
763
                $jobDescriptor->write();
764
                $broken = true;
765
            }
766
        }
767
768
        $errorHandler->clear();
769
770
        Config::unnest();
771
772
        // okay let's reset our user if we've got an original
773
        if ($runAsUser && $originalUser && Controller::has_curr()) {
774
            Controller::curr()->getRequest()->getSession()->clear("loggedInAs");
775
            if ($originalUser) {
776
                Controller::curr()->getRequest()->getSession()->set("loggedInAs", $originalUser->ID);
777
            }
778
        }
779
780
        return !$broken;
781
    }
782
783
    /**
784
     * Start timer
785
     */
786
    protected function markStarted()
787
    {
788
        if ($this->startedAt) {
789
            $this->startedAt = DBDatetime::now()->Format('U');
0 ignored issues
show
Documentation Bug introduced by
It seems like \SilverStripe\ORM\FieldT...ime::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...
790
        }
791
    }
792
793
    /**
794
     * Is execution time too long?
795
     *
796
     * @return bool True if the script has passed the configured time_limit
797
     */
798
    protected function hasPassedTimeLimit()
799
    {
800
        // Ensure a limit exists
801
        $limit = static::config()->get('time_limit');
802
        if (!$limit) {
803
            return false;
804
        }
805
806
        // Ensure started date is set
807
        $this->markStarted();
808
809
        // Check duration
810
        $now = DBDatetime::now()->Format('U');
811
        return $now > $this->startedAt + $limit;
812
    }
813
814
    /**
815
     * Is memory usage too high?
816
     *
817
     * @return bool
818
     */
819
    protected function isMemoryTooHigh()
820
    {
821
        $used = $this->getMemoryUsage();
822
        $limit = $this->getMemoryLimit();
823
        return $limit && ($used > $limit);
824
    }
825
826
    /**
827
     * Get peak memory usage of this application
828
     *
829
     * @return float
830
     */
831
    protected function getMemoryUsage()
832
    {
833
        // Note we use real_usage = false
834
        // http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
835
        // Also we use the safer peak memory usage
836
        return (float)memory_get_peak_usage(false);
837
    }
838
839
    /**
840
     * Determines the memory limit (in bytes) for this application
841
     * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
842
     *
843
     * @return float Memory limit in bytes
844
     */
845
    protected function getMemoryLimit()
846
    {
847
        // Limit to smaller of explicit limit or php memory limit
848
        $limit = $this->parseMemory(static::config()->get('memory_limit'));
849
        if ($limit) {
850
            return $limit;
851
        }
852
853
        // Fallback to php memory limit
854
        $phpLimit = $this->getPHPMemoryLimit();
855
        if ($phpLimit) {
856
            return $phpLimit;
857
        }
858
    }
859
860
    /**
861
     * Calculate the current memory limit of the server
862
     *
863
     * @return float
864
     */
865
    protected function getPHPMemoryLimit()
866
    {
867
        return $this->parseMemory(trim(ini_get("memory_limit")));
868
    }
869
870
    /**
871
     * Convert memory limit string to bytes.
872
     * Based on implementation in install.php5
873
     *
874
     * @param string $memString
875
     * @return float
876
     */
877
    protected function parseMemory($memString)
878
    {
879
        switch (strtolower(substr($memString, -1))) {
880
            case "b":
881
                return round(substr($memString, 0, -1));
882
            case "k":
883
                return round(substr($memString, 0, -1) * 1024);
884
            case "m":
885
                return round(substr($memString, 0, -1) * 1024 * 1024);
886
            case "g":
887
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
888
            default:
889
                return round($memString);
890
        }
891
    }
892
893
    protected function humanReadable($size)
894
    {
895
        $filesizename = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
896
        return $size ? round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
897
    }
898
899
900
    /**
901
     * Gets a list of all the current jobs (or jobs that have recently finished)
902
     *
903
     * @param string $type
904
     *          if we're after a particular job list
905
     * @param int $includeUpUntil
906
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
907
     *          includes recently finished jobs
908
     * @return QueuedJobDescriptor
909
     */
910
    public function getJobList($type = null, $includeUpUntil = 0)
911
    {
912
        return DataObject::get(
913
            QueuedJobDescriptor::class,
914
            $this->getJobListFilter($type, $includeUpUntil)
915
        );
916
    }
917
918
    /**
919
     * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
920
     *
921
     * @param string $type
922
     *          if we're after a particular job list
923
     * @param int $includeUpUntil
924
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
925
     *          includes recently finished jobs
926
     * @return string
927
     */
928
    public function getJobListFilter($type = null, $includeUpUntil = 0)
929
    {
930
        $util = singleton(QJUtils::class);
931
932
        $filter = array('JobStatus <>' => QueuedJob::STATUS_COMPLETE);
933
        if ($includeUpUntil) {
934
            $filter['JobFinished > '] = date('Y-m-d H:i:s', time() - $includeUpUntil);
935
        }
936
937
        $filter = $util->dbQuote($filter, ' OR ');
938
939
        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...
940
            $filter = $util->dbQuote(array('JobType =' => (string) $type)). ' AND ('.$filter.')';
941
        }
942
943
        return $filter;
944
    }
945
946
    /**
947
     * Process the job queue with the current queue runner
948
     *
949
     * @param string $queue
950
     */
951
    public function runQueue($queue)
952
    {
953
        $this->checkJobHealth($queue);
954
        $this->checkdefaultJobs($queue);
955
        $this->queueRunner->runQueue($queue);
956
    }
957
958
    /**
959
     * Process all jobs from a given queue
960
     *
961
     * @param string $name The job queue to completely process
962
     */
963
    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...
964
    {
965
        // Start timer to measure lifetime
966
        $this->markStarted();
967
968
        // Begin main loop
969
        do {
970
            if (class_exists('Subsite')) {
971
                // clear subsite back to default to prevent any subsite changes from leaking to
972
                // subsequent actions
973
                /**
974
                 * @todo Check for 4.x compatibility with Subsites once namespacing is implemented
975
                 */
976
                \Subsite::changeSubsite(0);
977
            }
978
            if (Controller::has_curr()) {
979
                Controller::curr()->getRequest()->getSession()->clear('loggedInAs');
980
            } else {
981
                unset($_SESSION['loggedInAs']);
982
            }
983
984
            if (class_exists('SecurityContext')) {
985
                singleton('SecurityContext')->setMember(null);
986
            }
987
988
            $job = $this->getNextPendingJob($name);
989
            if ($job) {
990
                $success = $this->runJob($job->ID);
991
                if (!$success) {
992
                    // make sure job is null so it doesn't continue the current
993
                    // processing loop. Next queue executor can pick up where
994
                    // things left off
995
                    $job = null;
996
                }
997
            }
998
        } while ($job);
999
    }
1000
1001
    /**
1002
     * When PHP shuts down, we want to process all of the immediate queue items
1003
     *
1004
     * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
1005
     * we ignore paused or stalled jobs.
1006
     */
1007
    public function onShutdown()
1008
    {
1009
        $this->processJobQueue(QueuedJob::IMMEDIATE);
1010
    }
1011
1012
    /**
1013
     * Get a logger
1014
     * @return LoggerInterface
1015
     */
1016
    public function getLogger()
1017
    {
1018
        return Injector::inst()->get(LoggerInterface::class);
1019
    }
1020
}
1021