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
Pull Request — master (#172)
by
unknown
01:45
created

QueuedJobService::setRunAsUser()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 31
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 13
nc 6
nop 2
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\Extensible;
14
use SilverStripe\Core\Injector\Injectable;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Dev\SapphireTest;
17
use SilverStripe\ORM\DataList;
18
use SilverStripe\ORM\DataObject;
19
use SilverStripe\ORM\DB;
20
use SilverStripe\ORM\FieldType\DBDatetime;
21
use SilverStripe\Security\Member;
22
use SilverStripe\Security\Permission;
23
use SilverStripe\Security\Security;
24
use Psr\Log\LoggerInterface;
25
use SilverStripe\Subsites\Model\Subsite;
26
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
27
use Symbiote\QueuedJobs\QJUtils;
28
29
/**
30
 * A service that can be used for starting, stopping and listing queued jobs.
31
 *
32
 * When a job is first added, it is initialised, its job type determined, then persisted to the database
33
 *
34
 * When the queues are scanned, a job is reloaded and processed. Ignoring the persistence and reloading, it looks
35
 * something like
36
 *
37
 * job->getJobType();
38
 * job->getJobData();
39
 * data->write();
40
 * job->setup();
41
 * while !job->isComplete
42
 *  job->process();
43
 *  job->getJobData();
44
 *  data->write();
45
 *
46
 *
47
 * @author Marcus Nyeholt <[email protected]>
48
 * @license BSD http://silverstripe.org/bsd-license/
49
 */
50
class QueuedJobService
51
{
52
    use Configurable;
53
    use Injectable;
54
    use Extensible;
55
56
    /**
57
     * @config
58
     * @var int
59
     */
60
    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...
61
62
    /**
63
     * How much ram will we allow before pausing and releasing the memory?
64
     *
65
     * For instance, set to 268435456 (256MB) to pause this process if used memory exceeds
66
     * this value. This needs to be set to a value lower than the php_ini max_memory as
67
     * the system will otherwise crash before shutdown can be handled gracefully.
68
     *
69
     * This was increased to 256MB for SilverStripe 4.x as framework uses more memory than 3.x
70
     *
71
     * @var int
72
     * @config
73
     */
74
    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...
75
76
    /**
77
     * Optional time limit (in seconds) to run the service before restarting to release resources.
78
     *
79
     * Defaults to no limit.
80
     *
81
     * @var int
82
     * @config
83
     */
84
    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...
85
86
    /**
87
     * Timestamp (in seconds) when the queue was started
88
     *
89
     * @var int
90
     */
91
    protected $startedAt = 0;
92
93
    /**
94
     * Should "immediate" jobs be managed using the shutdown function?
95
     *
96
     * It is recommended you set up an inotify watch and use that for
97
     * triggering immediate jobs. See the wiki for more information
98
     *
99
     * @var boolean
100
     * @config
101
     */
102
    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...
103
104
    /**
105
     * The location for immediate jobs to be stored in
106
     *
107
     * @var string
108
     * @config
109
     */
110
    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...
111
112
    /**
113
     * @var DefaultQueueHandler
114
     */
115
    public $queueHandler;
116
117
    /**
118
     *
119
     * @var TaskRunnerEngine
120
     */
121
    public $queueRunner;
122
123
    /**
124
     * Config controlled list of default/required jobs
125
     * @var array
126
     */
127
    public $defaultJobs = [];
128
129
    /**
130
     * Register our shutdown handler
131
     */
132
    public function __construct()
133
    {
134
        // bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
135
        if (static::config()->get('use_shutdown_function') && Director::is_cli()) {
136
            register_shutdown_function(array($this, 'onShutdown'));
137
        }
138
        if (Config::inst()->get(Email::class, 'queued_job_admin_email') == '') {
139
            Config::modify()->set(
140
                Email::class,
141
                'queued_job_admin_email',
142
                Config::inst()->get(Email::class, 'admin_email')
143
            );
144
        }
145
    }
146
147
    /**
148
     * Adds a job to the queue to be started
149
     *
150
     * Relevant data about the job will be persisted using a QueuedJobDescriptor
151
     *
152
     * @param QueuedJob $job
153
     *          The job to start.
154
     * @param $startAfter
155
     *          The date (in Y-m-d H:i:s format) to start execution after
156
     * @param int $userId
157
     *          The ID of a user to execute the job as. Defaults to the current user
158
     * @return int
159
     */
160
    public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
161
    {
162
        $signature = $job->getSignature();
163
164
        // see if we already have this job in a queue
165
        $filter = array(
166
            'Signature' => $signature,
167
            'JobStatus' => array(
168
                QueuedJob::STATUS_NEW,
169
                QueuedJob::STATUS_INIT,
170
            )
171
        );
172
173
        $existing = DataList::create(QueuedJobDescriptor::class)
174
            ->filter($filter)
175
            ->first();
176
177
        if ($existing && $existing->ID) {
178
            return $existing->ID;
179
        }
180
181
        $jobDescriptor = new QueuedJobDescriptor();
182
        $jobDescriptor->JobTitle = $job->getTitle();
183
        $jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
184
        $jobDescriptor->Signature = $signature;
185
        $jobDescriptor->Implementation = get_class($job);
186
        $jobDescriptor->StartAfter = $startAfter;
187
188
        if ($userId === null) {
189
            $userId = (Security::getCurrentUser() ? Security::getCurrentUser()->ID : null);
190
        }
191
192
        $jobDescriptor->RunAsID = $userId;
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...
193
194
        // copy data
195
        $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...
196
197
        $jobDescriptor->write();
198
199
        $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...
200
201
        return $jobDescriptor->ID;
202
    }
203
204
    /**
205
     * Start a job (or however the queue handler determines it should be started)
206
     *
207
     * @param JobDescriptor $jobDescriptor
208
     * @param date $startAfter
209
     */
210
    public function startJob($jobDescriptor, $startAfter = null)
211
    {
212
        if ($startAfter && strtotime($startAfter) > time()) {
213
            $this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
214
        } else {
215
            // immediately start it on the queue, however that works
216
            $this->queueHandler->startJobOnQueue($jobDescriptor);
217
        }
218
    }
219
220
    /**
221
     * Copies data from a job into a descriptor for persisting
222
     *
223
     * @param QueuedJob $job
224
     * @param JobDescriptor $jobDescriptor
225
     */
226
    protected function copyJobToDescriptor($job, $jobDescriptor)
227
    {
228
        $data = $job->getJobData();
229
230
        $jobDescriptor->TotalSteps = $data->totalSteps;
231
        $jobDescriptor->StepsProcessed = $data->currentStep;
232
        if ($data->isComplete) {
233
            $jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
234
            $jobDescriptor->JobFinished = date('Y-m-d H:i:s');
235
        }
236
237
        $jobDescriptor->SavedJobData = serialize($data->jobData);
238
        $jobDescriptor->SavedJobMessages = serialize($data->messages);
239
    }
240
241
    /**
242
     * @param QueuedJobDescriptor $jobDescriptor
243
     * @param QueuedJob $job
244
     */
245
    protected function copyDescriptorToJob($jobDescriptor, $job)
246
    {
247
        $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...
248
        $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...
249
250
        // switching to php's serialize methods... not sure why this wasn't done from the start!
251
        $jobData = @unserialize($jobDescriptor->SavedJobData);
252
        $messages = @unserialize($jobDescriptor->SavedJobMessages);
253
254
        if (!$jobData) {
255
            // SS's convert:: function doesn't do this detection for us!!
256
            if (function_exists('json_decode')) {
257
                $jobData = json_decode($jobDescriptor->SavedJobData);
258
                $messages = json_decode($jobDescriptor->SavedJobMessages);
259
            } else {
260
                $jobData = Convert::json2obj($jobDescriptor->SavedJobData);
261
                $messages = Convert::json2obj($jobDescriptor->SavedJobMessages);
262
            }
263
        }
264
265
        $job->setJobData(
266
            $jobDescriptor->TotalSteps,
267
            $jobDescriptor->StepsProcessed,
268
            $jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
269
            $jobData,
270
            $messages
271
        );
272
    }
273
274
    /**
275
     * Check the current job queues and see if any of the jobs currently in there should be started. If so,
276
     * return the next job that should be executed
277
     *
278
     * @param string $type Job type
279
     * @return QueuedJobDescriptor
280
     */
281
    public function getNextPendingJob($type = null)
282
    {
283
        // Filter jobs by type
284
        $type = $type ?: QueuedJob::QUEUED;
285
        $list = QueuedJobDescriptor::get()
286
            ->filter('JobType', $type)
287
            ->sort('ID', 'ASC');
288
289
        // see if there's any blocked jobs that need to be resumed
290
        $waitingJob = $list
291
            ->filter('JobStatus', QueuedJob::STATUS_WAIT)
292
            ->first();
293
        if ($waitingJob) {
294
            return $waitingJob;
295
        }
296
297
        // If there's an existing job either running or pending, the lets just return false to indicate
298
        // that we're still executing
299
        $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...
300
            ->filter('JobStatus', array(QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN))
301
            ->first();
302
        if ($runningJob) {
303
            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...
304
        }
305
306
        // Otherwise, lets find any 'new' jobs that are waiting to execute
307
        $newJob = $list
308
            ->filter('JobStatus', QueuedJob::STATUS_NEW)
309
            ->where(sprintf(
310
                '"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
311
                DBDatetime::now()->getValue()
312
            ))
313
            ->first();
314
315
        return $newJob;
316
    }
317
318
    /**
319
     * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
320
     * between each run. If it's not, then we need to flag it as paused due to an error.
321
     *
322
     * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
323
     * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
324
     * fix them
325
     *
326
     * @param int $queue The queue to check against
327
     */
328
    public function checkJobHealth($queue = null)
329
    {
330
        $queue = $queue ?: QueuedJob::QUEUED;
331
        // Select all jobs currently marked as running
332
        $runningJobs = QueuedJobDescriptor::get()
333
            ->filter(array(
334
                'JobStatus' => array(
335
                    QueuedJob::STATUS_RUN,
336
                    QueuedJob::STATUS_INIT,
337
                ),
338
                'JobType' => $queue,
339
            ));
340
341
        // If no steps have been processed since the last run, consider it a broken job
342
        // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
343
        $stalledJobs = $runningJobs
344
            ->filter('LastProcessedCount:GreaterThanOrEqual', 0)
345
            ->where('"StepsProcessed" = "LastProcessedCount"');
346
        foreach ($stalledJobs as $stalledJob) {
347
            $this->restartStalledJob($stalledJob);
348
        }
349
350
        // now, find those that need to be marked before the next check
351
        // foreach job, mark it as having been incremented
352
        foreach ($runningJobs as $job) {
353
            $job->LastProcessedCount = $job->StepsProcessed;
354
            $job->write();
355
        }
356
357
        // finally, find the list of broken jobs and send an email if there's some found
358
        $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
359 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...
360
            $this->getLogger()->error(
361
                print_r(
362
                    array(
363
                        'errno' => 0,
364
                        'errstr' => 'Broken jobs were found in the job queue',
365
                        'errfile' => __FILE__,
366
                        'errline' => __LINE__,
367
                        'errcontext' => array()
368
                    ),
369
                    true
370
                )
371
            );
372
        }
373
    }
374
375
    /**
376
     * Checks through ll the scheduled jobs that are expected to exist
377
     */
378
    public function checkDefaultJobs($queue = null)
379
    {
380
        $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...
381
        if (count($this->defaultJobs)) {
382
            $activeJobs = QueuedJobDescriptor::get()->filter(
383
                'JobStatus',
384
                array(
385
                    QueuedJob::STATUS_NEW,
386
                    QueuedJob::STATUS_INIT,
387
                    QueuedJob::STATUS_RUN,
388
                    QueuedJob::STATUS_WAIT,
389
                    QueuedJob::STATUS_PAUSED,
390
                )
391
            );
392
            foreach ($this->defaultJobs as $title => $jobConfig) {
393
                if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
394
                    $this->getLogger()->error("Default Job config: $title incorrectly set up. Please check the readme for examples");
395
                    continue;
396
                }
397
                $job = $activeJobs->filter(array_merge(
398
                    array('Implementation' => $jobConfig['type']),
399
                    $jobConfig['filter']
400
                ));
401
                if (!$job->count()) {
402
                    $this->getLogger()->error("Default Job config: $title was missing from Queue");
403
                    Email::create()
404
                        ->setTo(isset($jobConfig['email']) ? $jobConfig['email'] : Config::inst()->get('Email', 'queued_job_admin_email'))
405
                        ->setFrom(Config::inst()->get('Email', 'queued_job_admin_email'))
406
                        ->setSubject('Default Job "' . $title . '" missing')
407
                        ->setData($jobConfig)
408
                        ->addData('Title', $title)
409
                        ->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...
410
                        ->setHTMLTemplate('QueuedJobsDefaultJob')
411
                        ->send();
412
                    if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
413
                        if (!array_key_exists('construct', $jobConfig) || !isset($jobConfig['startDateFormat']) || !isset($jobConfig['startTimeString'])) {
414
                            $this->getLogger()->error("Default Job config: $title incorrectly set up. Please check the readme for examples");
415
                            continue;
416
                        }
417
                        singleton('Symbiote\\QueuedJobs\\Services\\QueuedJobService')->queueJob(
418
                            Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
419
                            date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
420
                        );
421
                        $this->getLogger()->error("Default Job config: $title has been re-added to the Queue");
422
                    }
423
                }
424
            }
425
        }
426
    }
427
428
    /**
429
     * Attempt to restart a stalled job
430
     *
431
     * @param QueuedJobDescriptor $stalledJob
432
     * @return bool True if the job was successfully restarted
433
     */
434
    protected function restartStalledJob($stalledJob)
435
    {
436
        if ($stalledJob->ResumeCounts < static::config()->get('stall_threshold')) {
437
            $stalledJob->restart();
438
            $message = _t(
439
                __CLASS__ . '.STALLED_JOB_RESTART_MSG',
440
                'A job named {name} appears to have stalled. It will be stopped and restarted, please login to make sure it has continued',
441
                ['name' => $stalledJob->JobTitle]
442
            );
443
        } else {
444
            $stalledJob->pause();
445
            $message = _t(
446
                __CLASS__ . '.STALLED_JOB_MSG',
447
                'A job named {name} appears to have stalled. It has been paused, please login to check it',
448
                ['name' => $stalledJob->JobTitle]
449
            );
450
        }
451
452
        $this->getLogger()->error($message);
453
        $from = Config::inst()->get(Email::class, 'admin_email');
454
        $to = Config::inst()->get(Email::class, 'queued_job_admin_email');
455
        $subject = _t(__CLASS__ . '.STALLED_JOB', 'Stalled job');
456
        $mail = new Email($from, $to, $subject, $message);
457
        $mail->send();
458
    }
459
460
    /**
461
     * Prepares the given jobDescriptor for execution. Returns the job that
462
     * will actually be run in a state ready for executing.
463
     *
464
     * Note that this is called each time a job is picked up to be executed from the cron
465
     * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
466
     * so your job MUST detect that and act accordingly.
467
     *
468
     * @param QueuedJobDescriptor $jobDescriptor
469
     *          The Job descriptor of a job to prepare for execution
470
     *
471
     * @return QueuedJob|boolean
472
     */
473
    protected function initialiseJob(QueuedJobDescriptor $jobDescriptor)
474
    {
475
        // create the job class
476
        $impl = $jobDescriptor->Implementation;
477
        $job = Injector::inst()->create($impl);
478
        /* @var $job QueuedJob */
479
        if (!$job) {
480
            throw new Exception("Implementation $impl no longer exists");
481
        }
482
483
        $jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
484
        $jobDescriptor->write();
485
486
        // make sure the data is there
487
        $this->copyDescriptorToJob($jobDescriptor, $job);
488
489
        // see if it needs 'setup' or 'restart' called
490
        if ($jobDescriptor->StepsProcessed <= 0) {
491
            $job->setup();
492
        } else {
493
            $job->prepareForRestart();
494
        }
495
496
        // make sure the descriptor is up to date with anything changed
497
        $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...
498
        $jobDescriptor->write();
499
500
        return $job;
501
    }
502
503
    /**
504
     * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
505
     * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
506
     * This may still have a race condition, but this should minimise the possibility.
507
     * Side effect is the job status will be changed to "Initialised".
508
     *
509
     * Assumption is the job has a status of "Queued" or "Wait".
510
     *
511
     * @param QueuedJobDescriptor $jobDescriptor
512
     * @return boolean
513
     */
514
    protected function grabMutex(QueuedJobDescriptor $jobDescriptor)
515
    {
516
        // write the status and determine if any rows were affected, for protection against a
517
        // potential race condition where two or more processes init the same job at once.
518
        // This deliberately does not use write() as that would always update LastEdited
519
        // and thus the row would always be affected.
520
        try {
521
            DB::query(sprintf(
522
                'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s',
523
                QueuedJob::STATUS_INIT,
524
                $jobDescriptor->ID
525
            ));
526
        } catch (Exception $e) {
527
            return false;
528
        }
529
530
        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...
531
            return false;
532
        }
533
534
        return true;
535
    }
536
537
    /**
538
     * Start the actual execution of a job.
539
     * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
540
     *
541
     * This method will continue executing until the job says it's completed
542
     *
543
     * @param int $jobId
544
     *          The ID of the job to start executing
545
     * @return boolean
546
     */
547
    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...
548
    {
549
        // first retrieve the descriptor
550
        $jobDescriptor = DataObject::get_by_id(
551
            QueuedJobDescriptor::class,
552
            (int) $jobId
553
        );
554
        if (!$jobDescriptor) {
555
            throw new Exception("$jobId is invalid");
556
        }
557
558
        // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
559
        // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
560
        // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
561
        // as the currently logged in user and hope for the best
562
563
        // We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
564
        // this point of execution in some circumstances
565
        $originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
566
        $originalUser = $originalUserID
567
            ? DataObject::get_by_id(Member::class, $originalUserID)
568
            : null;
569
        $runAsUser = null;
570
571
        // If we're running in CLI, or the user isn't logged in, or the logged in user isn't an ADMIN, we should check
572
        // to see whether or not we need to set $runAsUser. If a user is an ADMIN, then we know they can perform the
573
        // actions required from any Job, so we don't need to set $runAsUser.
574
        if (Director::is_cli() || !$originalUser || !Permission::checkMember($originalUser, 'ADMIN')) {
575
            $runAsUser = $this->setRunAsUser($jobDescriptor, $originalUser);
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...
Bug introduced by
It seems like $originalUser defined by $originalUserID ? \Silve...$originalUserID) : null on line 566 can also be of type object<SilverStripe\ORM\DataObject>; however, Symbiote\QueuedJobs\Serv...Service::setRunAsUser() does only seem to accept null|object<SilverStripe\Security\Member>, 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...
576
        }
577
578
        // set up a custom error handler for this processing
579
        $errorHandler = new JobErrorHandler();
580
581
        $job = null;
582
583
        $broken = false;
584
585
        // Push a config context onto the stack for the duration of this job run.
586
        Config::nest();
587
588
        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...
589
            try {
590
                $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...
591
592
                // get the job ready to begin.
593
                if (!$jobDescriptor->JobStarted) {
594
                    $jobDescriptor->JobStarted = date('Y-m-d H:i:s');
595
                } else {
596
                    $jobDescriptor->JobRestarted = date('Y-m-d H:i:s');
597
                }
598
599
                // Only write to job as "Running" if 'isComplete' was NOT set to true
600
                // 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...
601
                if (!$job->jobFinished()) {
602
                    $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
603
                    $jobDescriptor->write();
604
                }
605
606
                $lastStepProcessed = 0;
607
                // have we stalled at all?
608
                $stallCount = 0;
609
610
                if ($job->SubsiteID && class_exists(Subsite::class)) {
611
                    Subsite::changeSubsite($job->SubsiteID);
612
613
                    // lets set the base URL as far as Director is concerned so that our URLs are correct
614
                    $subsite = DataObject::get_by_id(Subsite::class, $job->SubsiteID);
615
                    if ($subsite && $subsite->exists()) {
616
                        $domain = $subsite->domain();
617
                        $base = rtrim(Director::protocol() . $domain, '/') . '/';
618
619
                        Config::modify()->set(Director::class, 'alternate_base_url', $base);
620
                    }
621
                }
622
623
                // while not finished
624
                while (!$job->jobFinished() && !$broken) {
625
                    // see that we haven't been set to 'paused' or otherwise by another process
626
                    $jobDescriptor = DataObject::get_by_id(
627
                        QueuedJobDescriptor::class,
628
                        (int) $jobId
629
                    );
630
                    if (!$jobDescriptor || !$jobDescriptor->exists()) {
631
                        $broken = true;
632
                        $this->getLogger()->error(
633
                            print_r(
634
                                array(
635
                                    'errno' => 0,
636
                                    'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
637
                                    'errfile' => __FILE__,
638
                                    'errline' => __LINE__,
639
                                    'errcontext' => array()
640
                                ),
641
                                true
642
                            )
643
                        );
644
                        break;
645
                    }
646
                    if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
647
                        // we've been paused by something, so we'll just exit
648
                        $job->addMessage(
649
                            _t(__CLASS__ . '.JOB_PAUSED', 'Job paused at {time}', ['time' => date('Y-m-d H:i:s')])
650
                        );
651
                        $broken = true;
652
                    }
653
654
                    if (!$broken) {
655
                        try {
656
                            $job->process();
657
                        } catch (Exception $e) {
658
                            // okay, we'll just catch this exception for now
659
                            $job->addMessage(
660
                                _t(
661
                                    __CLASS__ . '.JOB_EXCEPT',
662
                                    'Job caused exception {message} in {file} at line {line}',
663
                                    [
664
                                        'message' => $e->getMessage(),
665
                                        'file' => $e->getFile(),
666
                                        'line' => $e->getLine(),
667
                                    ]
668
                                ),
669
                                'ERROR'
670
                            );
671
                            $this->getLogger()->error($e->getMessage());
672
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
673
                            $this->extend('updateJobDescriptorAndJobOnException', $jobDescriptor, $job, $e);
674
                        }
675
676
                        // now check the job state
677
                        $data = $job->getJobData();
678
                        if ($data->currentStep == $lastStepProcessed) {
679
                            $stallCount++;
680
                        }
681
682
                        if ($stallCount > static::config()->get('stall_threshold')) {
683
                            $broken = true;
684
                            $job->addMessage(
685
                                _t(
686
                                    __CLASS__ . '.JOB_STALLED',
687
                                    'Job stalled after {attempts} attempts - please check',
688
                                    ['attempts' => $stallCount]
689
                                ),
690
                                'ERROR'
691
                            );
692
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
693
                        }
694
695
                        // now we'll be good and check our memory usage. If it is too high, we'll set the job to
696
                        // a 'Waiting' state, and let the next processing run pick up the job.
697
                        if ($this->isMemoryTooHigh()) {
698
                            $job->addMessage(
699
                                _t(
700
                                    __CLASS__ . '.MEMORY_RELEASE',
701
                                    'Job releasing memory and waiting ({used} used)',
702
                                    ['used' => $this->humanReadable($this->getMemoryUsage())]
703
                                )
704
                            );
705
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
706
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
707
                            }
708
                            $broken = true;
709
                        }
710
711
                        // Also check if we are running too long
712
                        if ($this->hasPassedTimeLimit()) {
713
                            $job->addMessage(_t(
714
                                __CLASS__ . '.TIME_LIMIT',
715
                                'Queue has passed time limit and will restart before continuing'
716
                            ));
717
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
718
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
719
                            }
720
                            $broken = true;
721
                        }
722
                    }
723
724
                    if ($jobDescriptor) {
725
                        $this->copyJobToDescriptor($job, $jobDescriptor);
726
                        $jobDescriptor->write();
727 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...
728
                        $this->getLogger()->error(
729
                            print_r(
730
                                array(
731
                                    'errno' => 0,
732
                                    'errstr' => 'Job descriptor has been set to null',
733
                                    'errfile' => __FILE__,
734
                                    'errline' => __LINE__,
735
                                    'errcontext' => array()
736
                                ),
737
                                true
738
                            )
739
                        );
740
                        $broken = true;
741
                    }
742
                }
743
744
                // a last final save. The job is complete by now
745
                if ($jobDescriptor) {
746
                    $jobDescriptor->write();
747
                }
748
749
                if ($job->jobFinished()) {
750
                    $job->afterComplete();
751
                    $jobDescriptor->cleanupJob();
752
                }
753
            } catch (Exception $e) {
754
                // okay, we'll just catch this exception for now
755
                $this->getLogger()->error($e->getMessage());
756
                $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
757
                $this->extend('updateJobDescriptorAndJobOnException', $jobDescriptor, $job, $e);
758
                $jobDescriptor->write();
759
                $broken = true;
760
            }
761
        }
762
763
        $errorHandler->clear();
764
765
        Config::unnest();
766
767
        $this->unsetRunAsUser($runAsUser, $originalUser);
0 ignored issues
show
Bug introduced by
It seems like $originalUser defined by $originalUserID ? \Silve...$originalUserID) : null on line 566 can also be of type object<SilverStripe\ORM\DataObject>; however, Symbiote\QueuedJobs\Serv...rvice::unsetRunAsUser() does only seem to accept null|object<SilverStripe\Security\Member>, 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...
768
769
        return !$broken;
770
    }
771
772
    /**
773
     * @param QueuedJobDescriptor $jobDescriptor
774
     * @param Member|null $originalUser
775
     * @return null
776
     */
777
    protected function setRunAsUser(QueuedJobDescriptor $jobDescriptor, Member $originalUser = null)
0 ignored issues
show
Coding Style introduced by
setRunAsUser 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...
778
    {
779
        /** @var Member $runAsUser */
780
        $runAsUser = $jobDescriptor->RunAs();
781
782
        // Don't set Security user if one wasn't provided.
783
        if (!$runAsUser || !$runAsUser->exists()) {
784
            return null;
785
        }
786
787
        // Don't need to set Security user if we're already logged in as that same user.
788
        if ($originalUser && $originalUser->ID === $runAsUser->ID) {
789
            return null;
790
        }
791
792
        // The job runner outputs content way early in the piece, meaning there'll be cookie errors if we try and do a
793
        // normal login, and we only want it temporarily...
794
        if (Controller::has_curr()) {
795
            Security::setCurrentUser($runAsUser);
796
        } else {
797
            $_SESSION['loggedInAs'] = $runAsUser->ID;
798
        }
799
800
        // This is an explicit coupling brought about by SS not having a nice way of mocking a user, as it requires
801
        // session nastiness
802
        if (class_exists('SecurityContext')) {
803
            singleton('SecurityContext')->setMember($runAsUser);
804
        }
805
806
        return $runAsUser;
807
    }
808
809
    /**
810
     * @param Member|null $runAsUser
811
     * @param Member|null $originalUser
812
     */
813
    protected function unsetRunAsUser(Member $runAsUser = null, Member $originalUser = null)
0 ignored issues
show
Coding Style introduced by
unsetRunAsUser 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...
814
    {
815
        // No runAsUser was set, so we don't need to do anything.
816
        if (!$runAsUser) {
817
            return;
818
        }
819
820
        // There was no originalUser, so we should make sure that we set the user back to null.
821
        if (!$originalUser) {
822
            if (Controller::has_curr()) {
823
                Security::setCurrentUser(null);
824
            } else {
825
                $_SESSION['loggedInAs'] = null;
826
            }
827
        }
828
829
        // Okay let's reset our user.
830
        if (Controller::has_curr()) {
831
            Security::setCurrentUser($originalUser);
832
        } else {
833
            $_SESSION['loggedInAs'] = $originalUser->ID;
834
        }
835
    }
836
837
    /**
838
     * Start timer
839
     */
840
    protected function markStarted()
841
    {
842
        if ($this->startedAt) {
843
            $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...
844
        }
845
    }
846
847
    /**
848
     * Is execution time too long?
849
     *
850
     * @return bool True if the script has passed the configured time_limit
851
     */
852
    protected function hasPassedTimeLimit()
853
    {
854
        // Ensure a limit exists
855
        $limit = static::config()->get('time_limit');
856
        if (!$limit) {
857
            return false;
858
        }
859
860
        // Ensure started date is set
861
        $this->markStarted();
862
863
        // Check duration
864
        $now = DBDatetime::now()->Format('U');
865
        return $now > $this->startedAt + $limit;
866
    }
867
868
    /**
869
     * Is memory usage too high?
870
     *
871
     * @return bool
872
     */
873
    protected function isMemoryTooHigh()
874
    {
875
        $used = $this->getMemoryUsage();
876
        $limit = $this->getMemoryLimit();
877
        return $limit && ($used > $limit);
878
    }
879
880
    /**
881
     * Get peak memory usage of this application
882
     *
883
     * @return float
884
     */
885
    protected function getMemoryUsage()
886
    {
887
        // Note we use real_usage = false
888
        // http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
889
        // Also we use the safer peak memory usage
890
        return (float)memory_get_peak_usage(false);
891
    }
892
893
    /**
894
     * Determines the memory limit (in bytes) for this application
895
     * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
896
     *
897
     * @return float Memory limit in bytes
898
     */
899
    protected function getMemoryLimit()
900
    {
901
        // Limit to smaller of explicit limit or php memory limit
902
        $limit = $this->parseMemory(static::config()->get('memory_limit'));
903
        if ($limit) {
904
            return $limit;
905
        }
906
907
        // Fallback to php memory limit
908
        $phpLimit = $this->getPHPMemoryLimit();
909
        if ($phpLimit) {
910
            return $phpLimit;
911
        }
912
    }
913
914
    /**
915
     * Calculate the current memory limit of the server
916
     *
917
     * @return float
918
     */
919
    protected function getPHPMemoryLimit()
920
    {
921
        return $this->parseMemory(trim(ini_get("memory_limit")));
922
    }
923
924
    /**
925
     * Convert memory limit string to bytes.
926
     * Based on implementation in install.php5
927
     *
928
     * @param string $memString
929
     * @return float
930
     */
931
    protected function parseMemory($memString)
932
    {
933
        switch (strtolower(substr($memString, -1))) {
934
            case "b":
935
                return round(substr($memString, 0, -1));
936
            case "k":
937
                return round(substr($memString, 0, -1) * 1024);
938
            case "m":
939
                return round(substr($memString, 0, -1) * 1024 * 1024);
940
            case "g":
941
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
942
            default:
943
                return round($memString);
944
        }
945
    }
946
947
    protected function humanReadable($size)
948
    {
949
        $filesizename = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
950
        return $size ? round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
951
    }
952
953
954
    /**
955
     * Gets a list of all the current jobs (or jobs that have recently finished)
956
     *
957
     * @param string $type
958
     *          if we're after a particular job list
959
     * @param int $includeUpUntil
960
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
961
     *          includes recently finished jobs
962
     * @return QueuedJobDescriptor
963
     */
964
    public function getJobList($type = null, $includeUpUntil = 0)
965
    {
966
        return DataObject::get(
967
            QueuedJobDescriptor::class,
968
            $this->getJobListFilter($type, $includeUpUntil)
969
        );
970
    }
971
972
    /**
973
     * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
974
     *
975
     * @param string $type
976
     *          if we're after a particular job list
977
     * @param int $includeUpUntil
978
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
979
     *          includes recently finished jobs
980
     * @return string
981
     */
982
    public function getJobListFilter($type = null, $includeUpUntil = 0)
983
    {
984
        $util = singleton(QJUtils::class);
985
986
        $filter = array('JobStatus <>' => QueuedJob::STATUS_COMPLETE);
987
        if ($includeUpUntil) {
988
            $filter['JobFinished > '] = date('Y-m-d H:i:s', time() - $includeUpUntil);
989
        }
990
991
        $filter = $util->dbQuote($filter, ' OR ');
992
993
        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...
994
            $filter = $util->dbQuote(array('JobType =' => (string) $type)). ' AND ('.$filter.')';
995
        }
996
997
        return $filter;
998
    }
999
1000
    /**
1001
     * Process the job queue with the current queue runner
1002
     *
1003
     * @param string $queue
1004
     */
1005
    public function runQueue($queue)
1006
    {
1007
        $this->checkJobHealth($queue);
1008
        $this->checkdefaultJobs($queue);
1009
        $this->queueRunner->runQueue($queue);
1010
    }
1011
1012
    /**
1013
     * Process all jobs from a given queue
1014
     *
1015
     * @param string $name The job queue to completely process
1016
     */
1017
    public function processJobQueue($name)
1018
    {
1019
        // Start timer to measure lifetime
1020
        $this->markStarted();
1021
1022
        // Begin main loop
1023
        do {
1024
            if (class_exists('Subsite')) {
1025
                // clear subsite back to default to prevent any subsite changes from leaking to
1026
                // subsequent actions
1027
                /**
1028
                 * @todo Check for 4.x compatibility with Subsites once namespacing is implemented
1029
                 */
1030
                \Subsite::changeSubsite(0);
1031
            }
1032
1033
            $job = $this->getNextPendingJob($name);
1034
            if ($job) {
1035
                $success = $this->runJob($job->ID);
1036
                if (!$success) {
1037
                    // make sure job is null so it doesn't continue the current
1038
                    // processing loop. Next queue executor can pick up where
1039
                    // things left off
1040
                    $job = null;
1041
                }
1042
            }
1043
        } while ($job);
1044
    }
1045
1046
    /**
1047
     * When PHP shuts down, we want to process all of the immediate queue items
1048
     *
1049
     * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
1050
     * we ignore paused or stalled jobs.
1051
     */
1052
    public function onShutdown()
1053
    {
1054
        $this->processJobQueue(QueuedJob::IMMEDIATE);
1055
    }
1056
1057
    /**
1058
     * Get a logger
1059
     * @return LoggerInterface
1060
     */
1061
    public function getLogger()
1062
    {
1063
        return Injector::inst()->get(LoggerInterface::class);
1064
    }
1065
}
1066