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 ( 6956e2...9a55f0 )
by
unknown
02:08
created

QueuedJobService::queueJob()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 8.2666
c 0
b 0
f 0
cc 7
nc 7
nop 4
1
<?php
2
3
namespace Symbiote\QueuedJobs\Services;
4
5
use Exception;
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Control\Email\Email;
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\ORM\DataList;
17
use SilverStripe\ORM\DataObject;
18
use SilverStripe\ORM\DB;
19
use SilverStripe\ORM\FieldType\DBDatetime;
20
use SilverStripe\Security\Member;
21
use SilverStripe\Security\Security;
22
use SilverStripe\Subsites\Model\Subsite;
23
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
24
use Symbiote\QueuedJobs\QJUtils;
25
use Symbiote\QueuedJobs\Tasks\Engines\TaskRunnerEngine;
26
27
/**
28
 * A service that can be used for starting, stopping and listing queued jobs.
29
 *
30
 * When a job is first added, it is initialised, its job type determined, then persisted to the database
31
 *
32
 * When the queues are scanned, a job is reloaded and processed. Ignoring the persistence and reloading, it looks
33
 * something like
34
 *
35
 * job->getJobType();
36
 * job->getJobData();
37
 * data->write();
38
 * job->setup();
39
 * while !job->isComplete
40
 *  job->process();
41
 *  job->getJobData();
42
 *  data->write();
43
 *
44
 *
45
 * @author Marcus Nyeholt <[email protected]>
46
 * @license BSD http://silverstripe.org/bsd-license/
47
 */
48
class QueuedJobService
49
{
50
    use Configurable;
51
    use Injectable;
52
    use Extensible;
53
54
    /**
55
     * @config
56
     * @var int
57
     */
58
    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...
59
60
    /**
61
     * How much ram will we allow before pausing and releasing the memory?
62
     *
63
     * For instance, set to 268435456 (256MB) to pause this process if used memory exceeds
64
     * this value. This needs to be set to a value lower than the php_ini max_memory as
65
     * the system will otherwise crash before shutdown can be handled gracefully.
66
     *
67
     * This was increased to 256MB for SilverStripe 4.x as framework uses more memory than 3.x
68
     *
69
     * @var int
70
     * @config
71
     */
72
    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...
73
74
    /**
75
     * Optional time limit (in seconds) to run the service before restarting to release resources.
76
     *
77
     * Defaults to no limit.
78
     *
79
     * @var int
80
     * @config
81
     */
82
    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...
83
84
    /**
85
     * Timestamp (in seconds) when the queue was started
86
     *
87
     * @var int
88
     */
89
    protected $startedAt = 0;
90
91
    /**
92
     * Should "immediate" jobs be managed using the shutdown function?
93
     *
94
     * It is recommended you set up an inotify watch and use that for
95
     * triggering immediate jobs. See the wiki for more information
96
     *
97
     * @var boolean
98
     * @config
99
     */
100
    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...
101
102
    /**
103
     * The location for immediate jobs to be stored in
104
     *
105
     * @var string
106
     * @config
107
     */
108
    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...
109
110
    /**
111
     * @var DefaultQueueHandler
112
     */
113
    public $queueHandler;
114
115
    /**
116
     *
117
     * @var TaskRunnerEngine
118
     */
119
    public $queueRunner;
120
121
    /**
122
     * Config controlled list of default/required jobs
123
     *
124
     * @var array
125
     */
126
    public $defaultJobs = [];
127
128
    /**
129
     * Register our shutdown handler
130
     */
131
    public function __construct()
132
    {
133
        // bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
134
        if (static::config()->get('use_shutdown_function') && Director::is_cli()) {
135
            register_shutdown_function([$this, 'onShutdown']);
136
        }
137
        if (Config::inst()->get(Email::class, 'queued_job_admin_email') == '') {
138
            Config::modify()->set(
139
                Email::class,
140
                'queued_job_admin_email',
141
                Config::inst()->get(Email::class, 'admin_email')
142
            );
143
        }
144
    }
145
146
    /**
147
     * Adds a job to the queue to be started
148
     *
149
     * Relevant data about the job will be persisted using a QueuedJobDescriptor
150
     *
151
     * @param QueuedJob $job
152
     *          The job to start.
153
     * @param $startAfter
154
     *          The date (in Y-m-d H:i:s format) to start execution after
155
     * @param int $userId
156
     *          The ID of a user to execute the job as. Defaults to the current user
157
     *
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 = [
166
            'Signature' => $signature,
167
            'JobStatus' => [
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
        $runAsID = 0;
189
        if ($userId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
190
            $runAsID = $userId;
191
        } elseif (Security::getCurrentUser() && Security::getCurrentUser()->exists()) {
192
            $runAsID = Security::getCurrentUser()->ID;
193
        }
194
        $jobDescriptor->RunAsID = $runAsID;
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...
195
196
        // copy data
197
        $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...
198
199
        $jobDescriptor->write();
200
201
        $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...
202
203
        return $jobDescriptor->ID;
204
    }
205
206
    /**
207
     * Start a job (or however the queue handler determines it should be started)
208
     *
209
     * @param JobDescriptor $jobDescriptor
210
     * @param date $startAfter
211
     */
212
    public function startJob($jobDescriptor, $startAfter = null)
213
    {
214
        if ($startAfter && strtotime($startAfter) > time()) {
215
            $this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
216
        } else {
217
            // immediately start it on the queue, however that works
218
            $this->queueHandler->startJobOnQueue($jobDescriptor);
219
        }
220
    }
221
222
    /**
223
     * Copies data from a job into a descriptor for persisting
224
     *
225
     * @param QueuedJob $job
226
     * @param JobDescriptor $jobDescriptor
227
     */
228
    protected function copyJobToDescriptor($job, $jobDescriptor)
229
    {
230
        $data = $job->getJobData();
231
232
        $jobDescriptor->TotalSteps = $data->totalSteps;
233
        $jobDescriptor->StepsProcessed = $data->currentStep;
234
        if ($data->isComplete) {
235
            $jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
236
            $jobDescriptor->JobFinished = date('Y-m-d H:i:s');
237
        }
238
239
        $jobDescriptor->SavedJobData = serialize($data->jobData);
240
        $jobDescriptor->SavedJobMessages = serialize($data->messages);
241
    }
242
243
    /**
244
     * @param QueuedJobDescriptor $jobDescriptor
245
     * @param QueuedJob $job
246
     */
247
    protected function copyDescriptorToJob($jobDescriptor, $job)
248
    {
249
        $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...
250
        $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...
251
252
        // switching to php's serialize methods... not sure why this wasn't done from the start!
253
        $jobData = @unserialize($jobDescriptor->SavedJobData);
254
        $messages = @unserialize($jobDescriptor->SavedJobMessages);
255
256
        if (!$jobData) {
257
            // SS's convert:: function doesn't do this detection for us!!
258
            if (function_exists('json_decode')) {
259
                $jobData = json_decode($jobDescriptor->SavedJobData);
260
                $messages = json_decode($jobDescriptor->SavedJobMessages);
261
            } else {
262
                $jobData = Convert::json2obj($jobDescriptor->SavedJobData);
263
                $messages = Convert::json2obj($jobDescriptor->SavedJobMessages);
264
            }
265
        }
266
267
        $job->setJobData(
268
            $jobDescriptor->TotalSteps,
269
            $jobDescriptor->StepsProcessed,
270
            $jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
271
            $jobData,
272
            $messages
273
        );
274
    }
275
276
    /**
277
     * Check the current job queues and see if any of the jobs currently in there should be started. If so,
278
     * return the next job that should be executed
279
     *
280
     * @param string $type Job type
281
     *
282
     * @return QueuedJobDescriptor
283
     */
284
    public function getNextPendingJob($type = null)
285
    {
286
        // Filter jobs by type
287
        $type = $type ?: QueuedJob::QUEUED;
288
        $list = QueuedJobDescriptor::get()
289
            ->filter('JobType', $type)
290
            ->sort('ID', 'ASC');
291
292
        // see if there's any blocked jobs that need to be resumed
293
        $waitingJob = $list
294
            ->filter('JobStatus', QueuedJob::STATUS_WAIT)
295
            ->first();
296
        if ($waitingJob) {
297
            return $waitingJob;
298
        }
299
300
        // If there's an existing job either running or pending, the lets just return false to indicate
301
        // that we're still executing
302
        $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...
303
            ->filter('JobStatus', [QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN])
304
            ->first();
305
        if ($runningJob) {
306
            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...
307
        }
308
309
        // Otherwise, lets find any 'new' jobs that are waiting to execute
310
        $newJob = $list
311
            ->filter('JobStatus', QueuedJob::STATUS_NEW)
312
            ->where(sprintf(
313
                '"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
314
                DBDatetime::now()->getValue()
315
            ))
316
            ->first();
317
318
        return $newJob;
319
    }
320
321
    /**
322
     * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
323
     * between each run. If it's not, then we need to flag it as paused due to an error.
324
     *
325
     * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
326
     * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
327
     * fix them
328
     *
329
     * @param int $queue The queue to check against
330
     */
331
    public function checkJobHealth($queue = null)
332
    {
333
        $queue = $queue ?: QueuedJob::QUEUED;
334
        // Select all jobs currently marked as running
335
        $runningJobs = QueuedJobDescriptor::get()
336
            ->filter([
337
                'JobStatus' => [
338
                    QueuedJob::STATUS_RUN,
339
                    QueuedJob::STATUS_INIT,
340
                ],
341
                'JobType' => $queue,
342
            ]);
343
344
        // If no steps have been processed since the last run, consider it a broken job
345
        // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
346
        $stalledJobs = $runningJobs
347
            ->filter('LastProcessedCount:GreaterThanOrEqual', 0)
348
            ->where('"StepsProcessed" = "LastProcessedCount"');
349
        foreach ($stalledJobs as $stalledJob) {
350
            $this->restartStalledJob($stalledJob);
351
        }
352
353
        // now, find those that need to be marked before the next check
354
        // foreach job, mark it as having been incremented
355
        foreach ($runningJobs as $job) {
356
            $job->LastProcessedCount = $job->StepsProcessed;
357
            $job->write();
358
        }
359
360
        // finally, find the list of broken jobs and send an email if there's some found
361
        $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
362 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...
363
            $this->getLogger()->error(
364
                print_r(
365
                    [
366
                        'errno' => 0,
367
                        'errstr' => 'Broken jobs were found in the job queue',
368
                        'errfile' => __FILE__,
369
                        'errline' => __LINE__,
370
                        'errcontext' => [],
371
                    ],
372
                    true
373
                ),
374
                [
375
                    'file' => __FILE__,
376
                    'line' => __LINE__,
377
                ]
378
            );
379
        }
380
    }
381
382
    /**
383
     * Checks through ll the scheduled jobs that are expected to exist
384
     */
385
    public function checkDefaultJobs($queue = null)
386
    {
387
        $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...
388
        if (count($this->defaultJobs)) {
389
            $activeJobs = QueuedJobDescriptor::get()->filter(
390
                'JobStatus',
391
                [
392
                    QueuedJob::STATUS_NEW,
393
                    QueuedJob::STATUS_INIT,
394
                    QueuedJob::STATUS_RUN,
395
                    QueuedJob::STATUS_WAIT,
396
                    QueuedJob::STATUS_PAUSED,
397
                ]
398
            );
399
            foreach ($this->defaultJobs as $title => $jobConfig) {
400
                if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
401
                    $this->getLogger()->error(
402
                        "Default Job config: $title incorrectly set up. Please check the readme for examples",
403
                        [
404
                            'file' => __FILE__,
405
                            'line' => __LINE__,
406
                        ]
407
                    );
408
                    continue;
409
                }
410
                $job = $activeJobs->filter(array_merge(
411
                    ['Implementation' => $jobConfig['type']],
412
                    $jobConfig['filter']
413
                ));
414
                if (!$job->count()) {
415
                    $this->getLogger()->error(
416
                        "Default Job config: $title was missing from Queue",
417
                        [
418
                            'file' => __FILE__,
419
                            'line' => __LINE__,
420
                        ]
421
                    );
422
                    Email::create()
423
                        ->setTo(isset($jobConfig['email']) ? $jobConfig['email'] : Config::inst()->get('Email', 'queued_job_admin_email'))
424
                        ->setFrom(Config::inst()->get('Email', 'queued_job_admin_email'))
425
                        ->setSubject('Default Job "' . $title . '" missing')
426
                        ->setData($jobConfig)
427
                        ->addData('Title', $title)
428
                        ->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...
429
                        ->setHTMLTemplate('QueuedJobsDefaultJob')
430
                        ->send();
431
                    if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
432
                        if (!array_key_exists('construct', $jobConfig) || !isset($jobConfig['startDateFormat']) || !isset($jobConfig['startTimeString'])) {
433
                            $this->getLogger()->error(
434
                                "Default Job config: $title incorrectly set up. Please check the readme for examples",
435
                                [
436
                                    'file' => __FILE__,
437
                                    'line' => __LINE__,
438
                                ]
439
                            );
440
                            continue;
441
                        }
442
                        singleton('Symbiote\\QueuedJobs\\Services\\QueuedJobService')->queueJob(
443
                            Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
444
                            date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
445
                        );
446
                        $this->getLogger()->error(
447
                            "Default Job config: $title has been re-added to the Queue",
448
                            [
449
                                'file' => __FILE__,
450
                                'line' => __LINE__,
451
                            ]
452
                        );
453
                    }
454
                }
455
            }
456
        }
457
    }
458
459
    /**
460
     * Attempt to restart a stalled job
461
     *
462
     * @param QueuedJobDescriptor $stalledJob
463
     *
464
     * @return bool True if the job was successfully restarted
465
     */
466
    protected function restartStalledJob($stalledJob)
467
    {
468
        if ($stalledJob->ResumeCounts < static::config()->get('stall_threshold')) {
469
            $stalledJob->restart();
470
            $message = _t(
471
                __CLASS__ . '.STALLED_JOB_RESTART_MSG',
472
                'A job named {name} appears to have stalled. It will be stopped and restarted, please login to make sure it has continued',
473
                ['name' => $stalledJob->JobTitle]
474
            );
475
        } else {
476
            $stalledJob->pause();
477
            $message = _t(
478
                __CLASS__ . '.STALLED_JOB_MSG',
479
                'A job named {name} appears to have stalled. It has been paused, please login to check it',
480
                ['name' => $stalledJob->JobTitle]
481
            );
482
        }
483
484
        $this->getLogger()->error(
485
            $message,
486
            [
487
                'file' => __FILE__,
488
                'line' => __LINE__,
489
            ]
490
        );
491
        $from = Config::inst()->get(Email::class, 'admin_email');
492
        $to = Config::inst()->get(Email::class, 'queued_job_admin_email');
493
        $subject = _t(__CLASS__ . '.STALLED_JOB', 'Stalled job');
494
        $mail = new Email($from, $to, $subject, $message);
495
        $mail->send();
496
    }
497
498
    /**
499
     * Prepares the given jobDescriptor for execution. Returns the job that
500
     * will actually be run in a state ready for executing.
501
     *
502
     * Note that this is called each time a job is picked up to be executed from the cron
503
     * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
504
     * so your job MUST detect that and act accordingly.
505
     *
506
     * @param QueuedJobDescriptor $jobDescriptor
507
     *          The Job descriptor of a job to prepare for execution
508
     *
509
     * @return QueuedJob|boolean
510
     */
511
    protected function initialiseJob(QueuedJobDescriptor $jobDescriptor)
512
    {
513
        // create the job class
514
        $impl = $jobDescriptor->Implementation;
515
        $job = Injector::inst()->create($impl);
516
        /* @var $job QueuedJob */
517
        if (!$job) {
518
            throw new Exception("Implementation $impl no longer exists");
519
        }
520
521
        $jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
522
        $jobDescriptor->write();
523
524
        // make sure the data is there
525
        $this->copyDescriptorToJob($jobDescriptor, $job);
526
527
        // see if it needs 'setup' or 'restart' called
528
        if ($jobDescriptor->StepsProcessed <= 0) {
529
            $job->setup();
530
        } else {
531
            $job->prepareForRestart();
532
        }
533
534
        // make sure the descriptor is up to date with anything changed
535
        $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...
536
        $jobDescriptor->write();
537
538
        return $job;
539
    }
540
541
    /**
542
     * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
543
     * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
544
     * This may still have a race condition, but this should minimise the possibility.
545
     * Side effect is the job status will be changed to "Initialised".
546
     *
547
     * Assumption is the job has a status of "Queued" or "Wait".
548
     *
549
     * @param QueuedJobDescriptor $jobDescriptor
550
     *
551
     * @return boolean
552
     */
553
    protected function grabMutex(QueuedJobDescriptor $jobDescriptor)
554
    {
555
        // write the status and determine if any rows were affected, for protection against a
556
        // potential race condition where two or more processes init the same job at once.
557
        // This deliberately does not use write() as that would always update LastEdited
558
        // and thus the row would always be affected.
559
        try {
560
            DB::query(sprintf(
561
                'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s',
562
                QueuedJob::STATUS_INIT,
563
                $jobDescriptor->ID
564
            ));
565
        } catch (Exception $e) {
566
            return false;
567
        }
568
569
        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...
570
            return false;
571
        }
572
573
        return true;
574
    }
575
576
    /**
577
     * Start the actual execution of a job.
578
     * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
579
     *
580
     * This method will continue executing until the job says it's completed
581
     *
582
     * @param int $jobId
583
     *          The ID of the job to start executing
584
     *
585
     * @return boolean
586
     */
587
    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...
588
    {
589
        // first retrieve the descriptor
590
        $jobDescriptor = DataObject::get_by_id(
591
            QueuedJobDescriptor::class,
592
            (int)$jobId
593
        );
594
        if (!$jobDescriptor) {
595
            throw new Exception("$jobId is invalid");
596
        }
597
598
        // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
599
        // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
600
        // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
601
        // as the currently logged in user and hope for the best
602
603
        // We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
604
        // this point of execution in some circumstances
605
        $originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
606
        $originalUser = $originalUserID
607
            ? DataObject::get_by_id(Member::class, $originalUserID)
608
            : null;
609
        $runAsUser = null;
610
611
        // If the Job has requested that we run it as a particular user, then we should try and do that.
612
        if ($jobDescriptor->RunAs() !== null) {
613
            $runAsUser = $this->setRunAsUser($jobDescriptor->RunAs(), $originalUser);
0 ignored issues
show
Bug introduced by
It seems like $originalUser defined by $originalUserID ? \Silve...$originalUserID) : null on line 606 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...
614
        }
615
616
        // set up a custom error handler for this processing
617
        $errorHandler = new JobErrorHandler();
618
619
        $job = null;
620
621
        $broken = false;
622
623
        // Push a config context onto the stack for the duration of this job run.
624
        Config::nest();
625
626
        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...
627
            try {
628
                $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...
629
630
                // get the job ready to begin.
631
                if (!$jobDescriptor->JobStarted) {
632
                    $jobDescriptor->JobStarted = date('Y-m-d H:i:s');
633
                } else {
634
                    $jobDescriptor->JobRestarted = date('Y-m-d H:i:s');
635
                }
636
637
                // Only write to job as "Running" if 'isComplete' was NOT set to true
638
                // 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...
639
                if (!$job->jobFinished()) {
640
                    $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
641
                    $jobDescriptor->write();
642
                }
643
644
                $lastStepProcessed = 0;
645
                // have we stalled at all?
646
                $stallCount = 0;
647
648
                if ($job->SubsiteID && class_exists(Subsite::class)) {
649
                    Subsite::changeSubsite($job->SubsiteID);
650
651
                    // lets set the base URL as far as Director is concerned so that our URLs are correct
652
                    /** @var Subsite $subsite */
653
                    $subsite = DataObject::get_by_id(Subsite::class, $job->SubsiteID);
654
                    if ($subsite && $subsite->exists()) {
655
                        $domain = $subsite->domain();
656
                        $base = rtrim(Director::protocol() . $domain, '/') . '/';
657
658
                        Config::modify()->set(Director::class, 'alternate_base_url', $base);
659
                    }
660
                }
661
662
                // while not finished
663
                while (!$job->jobFinished() && !$broken) {
664
                    // see that we haven't been set to 'paused' or otherwise by another process
665
                    $jobDescriptor = DataObject::get_by_id(
666
                        QueuedJobDescriptor::class,
667
                        (int)$jobId
668
                    );
669
                    if (!$jobDescriptor || !$jobDescriptor->exists()) {
670
                        $broken = true;
671
                        $this->getLogger()->error(
672
                            print_r(
673
                                [
674
                                    'errno' => 0,
675
                                    'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
676
                                    'errfile' => __FILE__,
677
                                    'errline' => __LINE__,
678
                                    'errcontext' => [],
679
                                ],
680
                                true
681
                            ),
682
                            [
683
                                'file' => __FILE__,
684
                                'line' => __LINE__,
685
                            ]
686
                        );
687
                        break;
688
                    }
689
                    if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
690
                        // we've been paused by something, so we'll just exit
691
                        $job->addMessage(
692
                            _t(__CLASS__ . '.JOB_PAUSED', 'Job paused at {time}', ['time' => date('Y-m-d H:i:s')])
693
                        );
694
                        $broken = true;
695
                    }
696
697
                    if (!$broken) {
698
                        // Collect output as job messages as well as sending it to the screen
699
                        $obLogger = function ($buffer, $phase) use ($job, $jobDescriptor) {
0 ignored issues
show
Unused Code introduced by
The parameter $phase is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
700
                            $job->addMessage($buffer);
701
                            if ($jobDescriptor) {
702
                                $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
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...
703
                                $jobDescriptor->write();
704
                            }
705
                            return $buffer;
706
                        };
707
                        ob_start($obLogger, 256);
708
709
                        try {
710
                            $job->process();
711
                        } catch (Exception $e) {
712
                            // okay, we'll just catch this exception for now
713
                            $job->addMessage(
714
                                _t(
715
                                    __CLASS__ . '.JOB_EXCEPT',
716
                                    'Job caused exception {message} in {file} at line {line}',
717
                                    [
718
                                        'message' => $e->getMessage(),
719
                                        'file' => $e->getFile(),
720
                                        'line' => $e->getLine(),
721
                                    ]
722
                                )
723
                            );
724
                            $this->getLogger()->error(
725
                                $e->getMessage(),
726
                                [
727
                                    'exception' => $e,
728
                                ]
729
                            );
730
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
731
                            $this->extend('updateJobDescriptorAndJobOnException', $jobDescriptor, $job, $e);
732
                        }
733
734
                        ob_end_flush();
735
736
                        // now check the job state
737
                        $data = $job->getJobData();
738
                        if ($data->currentStep == $lastStepProcessed) {
739
                            $stallCount++;
740
                        }
741
742
                        if ($stallCount > static::config()->get('stall_threshold')) {
743
                            $broken = true;
744
                            $job->addMessage(
745
                                _t(
746
                                    __CLASS__ . '.JOB_STALLED',
747
                                    'Job stalled after {attempts} attempts - please check',
748
                                    ['attempts' => $stallCount]
749
                                )
750
                            );
751
                            $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN;
752
                        }
753
754
                        // now we'll be good and check our memory usage. If it is too high, we'll set the job to
755
                        // a 'Waiting' state, and let the next processing run pick up the job.
756
                        if ($this->isMemoryTooHigh()) {
757
                            $job->addMessage(
758
                                _t(
759
                                    __CLASS__ . '.MEMORY_RELEASE',
760
                                    'Job releasing memory and waiting ({used} used)',
761
                                    ['used' => $this->humanReadable($this->getMemoryUsage())]
762
                                )
763
                            );
764
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
765
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
766
                            }
767
                            $broken = true;
768
                        }
769
770
                        // Also check if we are running too long
771
                        if ($this->hasPassedTimeLimit()) {
772
                            $job->addMessage(_t(
773
                                __CLASS__ . '.TIME_LIMIT',
774
                                'Queue has passed time limit and will restart before continuing'
775
                            ));
776
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
777
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
778
                            }
779
                            $broken = true;
780
                        }
781
                    }
782
783
                    if ($jobDescriptor) {
784
                        $this->copyJobToDescriptor($job, $jobDescriptor);
785
                        $jobDescriptor->write();
786 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...
787
                        $this->getLogger()->error(
788
                            print_r(
789
                                [
790
                                    'errno' => 0,
791
                                    'errstr' => 'Job descriptor has been set to null',
792
                                    'errfile' => __FILE__,
793
                                    'errline' => __LINE__,
794
                                    'errcontext' => [],
795
                                ],
796
                                true
797
                            ),
798
                            [
799
                                'file' => __FILE__,
800
                                'line' => __LINE__,
801
                            ]
802
                        );
803
                        $broken = true;
804
                    }
805
                }
806
807
                // a last final save. The job is complete by now
808
                if ($jobDescriptor) {
809
                    $jobDescriptor->write();
810
                }
811
812
                if ($job->jobFinished()) {
813
                    $job->afterComplete();
814
                    $jobDescriptor->cleanupJob();
815
                }
816
            } catch (Exception $e) {
817
                // okay, we'll just catch this exception for now
818
                $this->getLogger()->error(
819
                    $e->getMessage(),
820
                    [
821
                        'exception' => $e,
822
                    ]
823
                );
824
                $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
825
                $this->extend('updateJobDescriptorAndJobOnException', $jobDescriptor, $job, $e);
826
                $jobDescriptor->write();
827
                $broken = true;
828
            }
829
        }
830
831
        $errorHandler->clear();
832
833
        Config::unnest();
834
835
        $this->unsetRunAsUser($runAsUser, $originalUser);
0 ignored issues
show
Bug introduced by
It seems like $originalUser defined by $originalUserID ? \Silve...$originalUserID) : null on line 606 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...
836
837
        return !$broken;
838
    }
839
840
    /**
841
     * @param Member $runAsUser
842
     * @param Member|null $originalUser
843
     * @return null|Member
844
     */
845
    protected function setRunAsUser(Member $runAsUser, 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...
846
    {
847
        // Sanity check. Can't set the user if they don't exist.
848
        if ($runAsUser === null || !$runAsUser->exists()) {
849
            return null;
850
        }
851
852
        // Don't need to set Security user if we're already logged in as that same user.
853
        if ($originalUser && $originalUser->ID === $runAsUser->ID) {
854
            return null;
855
        }
856
857
        // We are currently either not logged in at all, or we're logged in as a different user. We should switch users
858
        // so that the context within the Job is correct.
859
        if (Controller::has_curr()) {
860
            Security::setCurrentUser($runAsUser);
861
        } else {
862
            $_SESSION['loggedInAs'] = $runAsUser->ID;
863
        }
864
865
        // This is an explicit coupling brought about by SS not having a nice way of mocking a user, as it requires
866
        // session nastiness
867
        if (class_exists('SecurityContext')) {
868
            singleton('SecurityContext')->setMember($runAsUser);
869
        }
870
871
        return $runAsUser;
872
    }
873
874
    /**
875
     * @param Member|null $runAsUser
876
     * @param Member|null $originalUser
877
     */
878
    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...
879
    {
880
        // No runAsUser was set, so we don't need to do anything.
881
        if ($runAsUser === null) {
882
            return;
883
        }
884
885
        // There was no originalUser, so we should make sure that we set the user back to null.
886
        if (!$originalUser) {
887
            if (Controller::has_curr()) {
888
                Security::setCurrentUser(null);
889
            } else {
890
                $_SESSION['loggedInAs'] = null;
891
            }
892
893
            return;
894
        }
895
896
        // Okay let's reset our user.
897
        if (Controller::has_curr()) {
898
            Security::setCurrentUser($originalUser);
899
        } else {
900
            $_SESSION['loggedInAs'] = $originalUser->ID;
901
        }
902
    }
903
904
    /**
905
     * Start timer
906
     */
907
    protected function markStarted()
908
    {
909
        if (!$this->startedAt) {
910
            $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...
911
        }
912
    }
913
914
    /**
915
     * Is execution time too long?
916
     *
917
     * @return bool True if the script has passed the configured time_limit
918
     */
919
    protected function hasPassedTimeLimit()
920
    {
921
        // Ensure a limit exists
922
        $limit = static::config()->get('time_limit');
923
        if (!$limit) {
924
            return false;
925
        }
926
927
        // Ensure started date is set
928
        $this->markStarted();
929
930
        // Check duration
931
        $now = DBDatetime::now()->Format('U');
932
        return $now > $this->startedAt + $limit;
933
    }
934
935
    /**
936
     * Is memory usage too high?
937
     *
938
     * @return bool
939
     */
940
    protected function isMemoryTooHigh()
941
    {
942
        $used = $this->getMemoryUsage();
943
        $limit = $this->getMemoryLimit();
944
        return $limit && ($used > $limit);
945
    }
946
947
    /**
948
     * Get peak memory usage of this application
949
     *
950
     * @return float
951
     */
952
    protected function getMemoryUsage()
953
    {
954
        // Note we use real_usage = false
955
        // http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
956
        // Also we use the safer peak memory usage
957
        return (float)memory_get_peak_usage(false);
958
    }
959
960
    /**
961
     * Determines the memory limit (in bytes) for this application
962
     * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
963
     *
964
     * @return float Memory limit in bytes
965
     */
966
    protected function getMemoryLimit()
967
    {
968
        // Limit to smaller of explicit limit or php memory limit
969
        $limit = $this->parseMemory(static::config()->get('memory_limit'));
970
        if ($limit) {
971
            return $limit;
972
        }
973
974
        // Fallback to php memory limit
975
        $phpLimit = $this->getPHPMemoryLimit();
976
        if ($phpLimit) {
977
            return $phpLimit;
978
        }
979
    }
980
981
    /**
982
     * Calculate the current memory limit of the server
983
     *
984
     * @return float
985
     */
986
    protected function getPHPMemoryLimit()
987
    {
988
        return $this->parseMemory(trim(ini_get("memory_limit")));
989
    }
990
991
    /**
992
     * Convert memory limit string to bytes.
993
     * Based on implementation in install.php5
994
     *
995
     * @param string $memString
996
     *
997
     * @return float
998
     */
999
    protected function parseMemory($memString)
1000
    {
1001
        switch (strtolower(substr($memString, -1))) {
1002
            case "b":
1003
                return round(substr($memString, 0, -1));
1004
            case "k":
1005
                return round(substr($memString, 0, -1) * 1024);
1006
            case "m":
1007
                return round(substr($memString, 0, -1) * 1024 * 1024);
1008
            case "g":
1009
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
1010
            default:
1011
                return round($memString);
1012
        }
1013
    }
1014
1015
    protected function humanReadable($size)
1016
    {
1017
        $filesizename = [" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"];
1018
        return $size ? round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
1019
    }
1020
1021
1022
    /**
1023
     * Gets a list of all the current jobs (or jobs that have recently finished)
1024
     *
1025
     * @param string $type
1026
     *          if we're after a particular job list
1027
     * @param int $includeUpUntil
1028
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
1029
     *          includes recently finished jobs
1030
     *
1031
     * @return DataList
1032
     */
1033
    public function getJobList($type = null, $includeUpUntil = 0)
1034
    {
1035
        return DataObject::get(
1036
            QueuedJobDescriptor::class,
1037
            $this->getJobListFilter($type, $includeUpUntil)
1038
        );
1039
    }
1040
1041
    /**
1042
     * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
1043
     *
1044
     * @param string $type
1045
     *          if we're after a particular job list
1046
     * @param int $includeUpUntil
1047
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
1048
     *          includes recently finished jobs
1049
     *
1050
     * @return string
1051
     */
1052
    public function getJobListFilter($type = null, $includeUpUntil = 0)
1053
    {
1054
        $util = singleton(QJUtils::class);
1055
1056
        $filter = ['JobStatus <>' => QueuedJob::STATUS_COMPLETE];
1057
        if ($includeUpUntil) {
1058
            $filter['JobFinished > '] = date('Y-m-d H:i:s', time() - $includeUpUntil);
1059
        }
1060
1061
        $filter = $util->dbQuote($filter, ' OR ');
1062
1063
        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...
1064
            $filter = $util->dbQuote(['JobType =' => (string)$type]) . ' AND (' . $filter . ')';
1065
        }
1066
1067
        return $filter;
1068
    }
1069
1070
    /**
1071
     * Process the job queue with the current queue runner
1072
     *
1073
     * @param string $queue
1074
     */
1075
    public function runQueue($queue)
1076
    {
1077
        $this->checkJobHealth($queue);
1078
        $this->checkdefaultJobs($queue);
1079
        $this->queueRunner->runQueue($queue);
1080
    }
1081
1082
    /**
1083
     * Process all jobs from a given queue
1084
     *
1085
     * @param string $name The job queue to completely process
1086
     */
1087
    public function processJobQueue($name)
1088
    {
1089
        // Start timer to measure lifetime
1090
        $this->markStarted();
1091
1092
        // Begin main loop
1093
        do {
1094
            if (class_exists(Subsite::class)) {
1095
                // clear subsite back to default to prevent any subsite changes from leaking to
1096
                // subsequent actions
1097
                Subsite::changeSubsite(0);
1098
            }
1099
1100
            $job = $this->getNextPendingJob($name);
1101
            if ($job) {
1102
                $success = $this->runJob($job->ID);
1103
                if (!$success) {
1104
                    // make sure job is null so it doesn't continue the current
1105
                    // processing loop. Next queue executor can pick up where
1106
                    // things left off
1107
                    $job = null;
1108
                }
1109
            }
1110
        } while ($job);
1111
    }
1112
1113
    /**
1114
     * When PHP shuts down, we want to process all of the immediate queue items
1115
     *
1116
     * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
1117
     * we ignore paused or stalled jobs.
1118
     */
1119
    public function onShutdown()
1120
    {
1121
        $this->processJobQueue(QueuedJob::IMMEDIATE);
1122
    }
1123
1124
    /**
1125
     * Get a logger
1126
     *
1127
     * @return LoggerInterface
1128
     */
1129
    public function getLogger()
1130
    {
1131
        return Injector::inst()->get(LoggerInterface::class);
1132
    }
1133
}
1134