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 (#255)
by
unknown
01:26
created

QueuedJobService::lockFilePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Symbiote\QueuedJobs\Services;
4
5
use Exception;
6
use Monolog\Handler\BufferHandler;
7
use Monolog\Logger;
8
use Psr\Log\LoggerInterface;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\Email\Email;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Core\Config\Configurable;
14
use SilverStripe\Core\Extensible;
15
use SilverStripe\Core\Injector\Injectable;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\ORM\DataList;
18
use SilverStripe\ORM\DataObject;
19
use SilverStripe\ORM\DB;
20
use SilverStripe\ORM\FieldType\DBDatetime;
21
use SilverStripe\ORM\ValidationException;
22
use SilverStripe\Security\Member;
23
use SilverStripe\Security\Security;
24
use SilverStripe\Subsites\Model\Subsite;
25
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
26
use Symbiote\QueuedJobs\QJUtils;
27
use Symbiote\QueuedJobs\Tasks\Engines\TaskRunnerEngine;
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;
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;
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;
85
86
    /**
87
     * Disable health checks that usually occur when a runner first picks up a queue. Note that completely disabling
88
     * health checks could result in many jobs that are always marked as running - that will never be restarted. If
89
     * this option is disabled you may alternatively use the build task
90
     *
91
     * @see \Symbiote\QueuedJobs\Tasks\CheckJobHealthTask
92
     *
93
     * @var bool
94
     * @config
95
     */
96
    private static $disable_health_check = false;
97
98
    /**
99
     * Maximum number of jobs that can be initialised at any one time.
100
     *
101
     * Prevents too many jobs getting into this state in case something goes wrong with the child processes.
102
     * We shouldn't have too many jobs in the initialising state anyway.
103
     *
104
     * Valid values:
105
     * 0 - unlimited (default)
106
     * greater than 0 - maximum number of jobs in initialised state
107
     *
108
     * @var int
109
     * @config
110
     */
111
    private static $max_init_jobs = 0;
112
113
    /**
114
     * Timestamp (in seconds) when the queue was started
115
     *
116
     * @var int
117
     */
118
    protected $startedAt = 0;
119
120
    /**
121
     * Should "immediate" jobs be managed using the shutdown function?
122
     *
123
     * It is recommended you set up an inotify watch and use that for
124
     * triggering immediate jobs. See the wiki for more information
125
     *
126
     * @var boolean
127
     * @config
128
     */
129
    private static $use_shutdown_function = true;
130
131
    /**
132
     * The location for immediate jobs to be stored in
133
     *
134
     * @var string
135
     * @config
136
     */
137
    private static $cache_dir = 'queuedjobs';
138
139
    /**
140
     * Maintenance lock file feature enabled / disable setting
141
     *
142
     * @config
143
     * @var bool
144
     */
145
    private static $lock_file_enabled = false;
146
147
    /**
148
     * Maintenance lock file name
149
     *
150
     * @config
151
     * @var string
152
     */
153
    private static $lock_file_name = 'maintenance-lock.txt';
154
155
    /**
156
     * Maintenance lock path (relative path starting at the base folder)
157
     *
158
     * @config
159
     * @var string
160
     */
161
    private static $lock_file_path = '';
162
163
    /**
164
     * @var DefaultQueueHandler
165
     */
166
    public $queueHandler;
167
168
    /**
169
     *
170
     * @var TaskRunnerEngine
171
     */
172
    public $queueRunner;
173
174
    /**
175
     * Config controlled list of default/required jobs
176
     *
177
     * @var array
178
     */
179
    public $defaultJobs = [];
180
181
    /**
182
     * Register our shutdown handler
183
     */
184
    public function __construct()
185
    {
186
        // bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
187
        if (static::config()->get('use_shutdown_function') && Director::is_cli()) {
188
            register_shutdown_function([$this, 'onShutdown']);
189
        }
190
191
        $queuedEmail = Config::inst()->get(Email::class, 'queued_job_admin_email');
192
193
        // if not set (and not explictly set to false), fallback to the admin email.
194
        if (!$queuedEmail && $queuedEmail !== false) {
195
            Config::modify()->set(
196
                Email::class,
197
                'queued_job_admin_email',
198
                Config::inst()->get(Email::class, 'admin_email')
199
            );
200
        }
201
    }
202
203
    /**
204
     * Adds a job to the queue to be started
205
     *
206
     * @param QueuedJob $job The job to start.
207
     * @param null|string $startAfter The date (in Y-m-d H:i:s format) to start execution after
208
     * @param null|int $userId The ID of a user to execute the job as. Defaults to the current user
209
     * @param null|int $queueName
210
     * @return int
211
     * @throws ValidationException
212
     */
213
    public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
214
    {
215
        $signature = $job->getSignature();
216
217
        // see if we already have this job in a queue
218
        $filter = [
219
            'Signature' => $signature,
220
            'JobStatus' => [
221
                QueuedJob::STATUS_NEW,
222
                QueuedJob::STATUS_INIT,
223
            ],
224
        ];
225
226
        $existing = QueuedJobDescriptor::get()
227
            ->filter($filter)
228
            ->first();
229
230
        if ($existing && $existing->ID) {
231
            return $existing->ID;
232
        }
233
234
        $jobDescriptor = new QueuedJobDescriptor();
235
        $jobDescriptor->JobTitle = $job->getTitle();
236
        $jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
237
        $jobDescriptor->Signature = $signature;
238
        $jobDescriptor->Implementation = get_class($job);
239
        $jobDescriptor->StartAfter = $startAfter;
240
241
        // no user provided - fallback to job user default
242
        if ($userId === null) {
243
            $userId = $job->getRunAsMemberID();
244
        }
245
246
        // still no user - fallback to current user
247
        if ($userId === null) {
248
            if (Security::getCurrentUser() && Security::getCurrentUser()->exists()) {
249
                // current user available
250
                $runAsID = Security::getCurrentUser()->ID;
251
            } else {
252
                // current user unavailable
253
                $runAsID = 0;
254
            }
255
        } else {
256
            $runAsID = $userId;
257
        }
258
259
        $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...
260
261
        // use this to populate custom data columns before job is queued
262
        // note: you can pass arbitrary data to your job and then move it to job descriptor
263
        // this is useful if you need some data that needs to be exposed as a separate
264
        // DB column as opposed to serialised data
265
        $this->extend('updateJobDescriptorBeforeQueued', $jobDescriptor, $job);
266
267
        // copy data
268
        $this->copyJobToDescriptor($job, $jobDescriptor);
269
270
        $jobDescriptor->write();
271
272
        $this->startJob($jobDescriptor, $startAfter);
273
274
        return $jobDescriptor->ID;
275
    }
276
277
    /**
278
     * Start a job (or however the queue handler determines it should be started)
279
     *
280
     * @param QueuedJobDescriptor $jobDescriptor
281
     * @param string $startAfter
282
     */
283
    public function startJob($jobDescriptor, $startAfter = null)
284
    {
285
        if ($startAfter && strtotime($startAfter) > DBDatetime::now()->getTimestamp()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $startAfter 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...
286
            $this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
287
        } else {
288
            // immediately start it on the queue, however that works
289
            $this->queueHandler->startJobOnQueue($jobDescriptor);
290
        }
291
    }
292
293
    /**
294
     * Check if maximum number of jobs are currently initialised.
295
     *
296
     * @return bool
297
     */
298
    public function isAtMaxJobs()
299
    {
300
        $initJobsMax = $this->config()->get('max_init_jobs');
301
        if (!$initJobsMax) {
302
            return false;
303
        }
304
305
        $initJobsCount = QueuedJobDescriptor::get()
306
            ->filter(['JobStatus' => QueuedJob::STATUS_INIT])
307
            ->count();
308
309
        if ($initJobsCount >= $initJobsMax) {
310
            return true;
311
        }
312
313
        return false;
314
    }
315
316
    /**
317
     * Copies data from a job into a descriptor for persisting
318
     *
319
     * @param QueuedJob $job
320
     * @param QueuedJobDescriptor $jobDescriptor
321
     */
322
    protected function copyJobToDescriptor($job, $jobDescriptor)
323
    {
324
        $data = $job->getJobData();
325
326
        $jobDescriptor->TotalSteps = $data->totalSteps;
327
        $jobDescriptor->StepsProcessed = $data->currentStep;
328
        if ($data->isComplete) {
329
            $jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
330
            $jobDescriptor->JobFinished = DBDatetime::now()->Rfc2822();
331
        }
332
333
        $jobDescriptor->SavedJobData = serialize($data->jobData);
334
        $jobDescriptor->SavedJobMessages = serialize($data->messages);
335
    }
336
337
    /**
338
     * @param QueuedJobDescriptor $jobDescriptor
339
     * @param QueuedJob $job
340
     */
341
    protected function copyDescriptorToJob($jobDescriptor, $job)
342
    {
343
        $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...
344
        $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...
345
346
        // switching to php's serialize methods... not sure why this wasn't done from the start!
347
        $jobData = @unserialize($jobDescriptor->SavedJobData);
348
        $messages = @unserialize($jobDescriptor->SavedJobMessages);
349
350
        // try decoding as json if null
351
        $jobData = $jobData ?: json_decode($jobDescriptor->SavedJobData);
352
        $messages = $messages ?: json_decode($jobDescriptor->SavedJobMessages);
353
354
        $job->setJobData(
355
            $jobDescriptor->TotalSteps,
356
            $jobDescriptor->StepsProcessed,
357
            $jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
358
            $jobData,
359
            $messages
360
        );
361
    }
362
363
    /**
364
     * Check the current job queues and see if any of the jobs currently in there should be started. If so,
365
     * return the next job that should be executed
366
     *
367
     * @param string $type Job type
368
     *
369
     * @return QueuedJobDescriptor|false
370
     */
371
    public function getNextPendingJob($type = null)
372
    {
373
        // Filter jobs by type
374
        $type = $type ?: QueuedJob::QUEUED;
375
        $list = QueuedJobDescriptor::get()
376
            ->filter('JobType', $type)
377
            ->sort('ID', 'ASC');
378
379
        // see if there's any blocked jobs that need to be resumed
380
        /** @var QueuedJobDescriptor $waitingJob */
381
        $waitingJob = $list
382
            ->filter('JobStatus', QueuedJob::STATUS_WAIT)
383
            ->first();
384
        if ($waitingJob) {
385
            return $waitingJob;
386
        }
387
388
        // If there's an existing job either running or pending, the lets just return false to indicate
389
        // that we're still executing
390
        $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...
391
            ->filter('JobStatus', [QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN])
392
            ->first();
393
        if ($runningJob) {
394
            return false;
395
        }
396
397
        // Otherwise, lets find any 'new' jobs that are waiting to execute
398
        /** @var QueuedJobDescriptor $newJob */
399
        $newJob = $list
400
            ->filter('JobStatus', QueuedJob::STATUS_NEW)
401
            ->where(sprintf(
402
                '"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
403
                DBDatetime::now()->getValue()
404
            ))
405
            ->first();
406
407
        return $newJob;
408
    }
409
410
    /**
411
     * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
412
     * between each run. If it's not, then we need to flag it as paused due to an error.
413
     *
414
     * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
415
     * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
416
     * fix them
417
     *
418
     * @param int $queue The queue to check against
419
     */
420
    public function checkJobHealth($queue = null)
421
    {
422
        $queue = $queue ?: QueuedJob::QUEUED;
423
        // Select all jobs currently marked as running
424
        $runningJobs = QueuedJobDescriptor::get()
425
            ->filter([
426
                'JobStatus' => [
427
                    QueuedJob::STATUS_RUN,
428
                    QueuedJob::STATUS_INIT,
429
                ],
430
                'JobType' => $queue,
431
            ]);
432
433
        // If no steps have been processed since the last run, consider it a broken job
434
        // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
435
        $stalledJobs = $runningJobs
436
            ->filter('LastProcessedCount:GreaterThanOrEqual', 0)
437
            ->where('"StepsProcessed" = "LastProcessedCount"');
438
        foreach ($stalledJobs as $stalledJob) {
439
            $this->restartStalledJob($stalledJob);
440
        }
441
442
        // now, find those that need to be marked before the next check
443
        // foreach job, mark it as having been incremented
444
        foreach ($runningJobs as $job) {
445
            $job->LastProcessedCount = $job->StepsProcessed;
446
            $job->write();
447
        }
448
449
        // finally, find the list of broken jobs and send an email if there's some found
450
        $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
451 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...
452
            $this->getLogger()->error(
453
                print_r(
454
                    [
455
                        'errno' => 0,
456
                        'errstr' => 'Broken jobs were found in the job queue',
457
                        'errfile' => __FILE__,
458
                        'errline' => __LINE__,
459
                        'errcontext' => [],
460
                    ],
461
                    true
462
                ),
463
                [
464
                    'file' => __FILE__,
465
                    'line' => __LINE__,
466
                ]
467
            );
468
        }
469
470
        return $stalledJobs->count();
471
    }
472
473
    /**
474
     * Checks through ll the scheduled jobs that are expected to exist
475
     */
476
    public function checkDefaultJobs($queue = null)
477
    {
478
        $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...
479
        if (count($this->defaultJobs)) {
480
            $activeJobs = QueuedJobDescriptor::get()->filter(
481
                'JobStatus',
482
                [
483
                    QueuedJob::STATUS_NEW,
484
                    QueuedJob::STATUS_INIT,
485
                    QueuedJob::STATUS_RUN,
486
                    QueuedJob::STATUS_WAIT,
487
                    QueuedJob::STATUS_PAUSED,
488
                ]
489
            );
490
            foreach ($this->defaultJobs as $title => $jobConfig) {
491
                if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
492
                    $this->getLogger()->error(
493
                        "Default Job config: $title incorrectly set up. Please check the readme for examples",
494
                        [
495
                            'file' => __FILE__,
496
                            'line' => __LINE__,
497
                        ]
498
                    );
499
                    continue;
500
                }
501
                $job = $activeJobs->filter(array_merge(
502
                    ['Implementation' => $jobConfig['type']],
503
                    $jobConfig['filter']
504
                ));
505
                if (!$job->count()) {
506
                    $this->getLogger()->info(
507
                        "Default Job config: $title was missing from Queue",
508
                        [
509
                            'file' => __FILE__,
510
                            'line' => __LINE__,
511
                        ]
512
                    );
513
514
                    $to = isset($jobConfig['email'])
515
                        ? $jobConfig['email']
516
                        : Config::inst()->get('Email', 'queued_job_admin_email');
517
518
                    if ($to) {
519
                        Email::create()
520
                            ->setTo($to)
521
                            ->setFrom(Config::inst()->get('Email', 'queued_job_admin_email'))
522
                            ->setSubject('Default Job "' . $title . '" missing')
523
                            ->setData($jobConfig)
524
                            ->addData('Title', $title)
525
                            ->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...
526
                            ->setHTMLTemplate('QueuedJobsDefaultJob')
527
                            ->send();
528
                    }
529
530
                    if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
531
                        if (!array_key_exists('construct', $jobConfig)
532
                            || !isset($jobConfig['startDateFormat'])
533
                            || !isset($jobConfig['startTimeString'])
534
                        ) {
535
                            $this->getLogger()->error(
536
                                "Default Job config: $title incorrectly set up. Please check the readme for examples",
537
                                [
538
                                    'file' => __FILE__,
539
                                    'line' => __LINE__,
540
                                ]
541
                            );
542
                            continue;
543
                        }
544
                        QueuedJobService::singleton()->queueJob(
545
                            Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
546
                            date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
547
                        );
548
                        $this->getLogger()->info(
549
                            "Default Job config: $title has been re-added to the Queue",
550
                            [
551
                                'file' => __FILE__,
552
                                'line' => __LINE__,
553
                            ]
554
                        );
555
                    }
556
                }
557
            }
558
        }
559
    }
560
561
    /**
562
     * Attempt to restart a stalled job
563
     *
564
     * @param QueuedJobDescriptor $stalledJob
565
     *
566
     * @return bool True if the job was successfully restarted
567
     */
568
    protected function restartStalledJob($stalledJob)
569
    {
570
        if ($stalledJob->ResumeCounts < static::config()->get('stall_threshold')) {
571
            $stalledJob->restart();
572
            $logLevel = 'warning';
573
            $message = _t(
574
                __CLASS__ . '.STALLED_JOB_RESTART_MSG',
575
                'A job named {name} (#{id}) appears to have stalled. It will be stopped and restarted, please '
576
                . 'login to make sure it has continued',
577
                ['name' => $stalledJob->JobTitle, 'id' => $stalledJob->ID]
578
            );
579
        } else {
580
            $stalledJob->pause();
581
            $logLevel = 'error';
582
            $message = _t(
583
                __CLASS__ . '.STALLED_JOB_MSG',
584
                'A job named {name} (#{id}) appears to have stalled. It has been paused, please login to check it',
585
                ['name' => $stalledJob->JobTitle, 'id' => $stalledJob->ID]
586
            );
587
        }
588
589
        $this->getLogger()->log(
590
            $logLevel,
591
            $message,
592
            [
593
                'file' => __FILE__,
594
                'line' => __LINE__,
595
            ]
596
        );
597
        $from = Config::inst()->get(Email::class, 'admin_email');
598
        $to = Config::inst()->get(Email::class, 'queued_job_admin_email');
599
        $subject = _t(__CLASS__ . '.STALLED_JOB', 'Stalled job');
600
601
        if ($to) {
602
            $mail = Email::create($from, $to, $subject)
603
                ->setData([
604
                    'JobID' => $stalledJob->ID,
605
                    'Message' => $message,
606
                    'Site' => Director::absoluteBaseURL(),
607
                ])
608
                ->setHTMLTemplate('QueuedJobsStalledJob');
609
            $mail->send();
610
        }
611
    }
612
613
    /**
614
     * Prepares the given jobDescriptor for execution. Returns the job that
615
     * will actually be run in a state ready for executing.
616
     *
617
     * Note that this is called each time a job is picked up to be executed from the cron
618
     * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
619
     * so your job MUST detect that and act accordingly.
620
     *
621
     * @param QueuedJobDescriptor $jobDescriptor
622
     *          The Job descriptor of a job to prepare for execution
623
     *
624
     * @return QueuedJob|boolean
625
     * @throws Exception
626
     */
627
    protected function initialiseJob(QueuedJobDescriptor $jobDescriptor)
628
    {
629
        // create the job class
630
        $impl = $jobDescriptor->Implementation;
631
        /** @var QueuedJob $job */
632
        $job = Injector::inst()->create($impl);
633
        /* @var $job QueuedJob */
634
        if (!$job) {
635
            throw new Exception("Implementation $impl no longer exists");
636
        }
637
638
        $jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
639
        $jobDescriptor->write();
640
641
        // make sure the data is there
642
        $this->copyDescriptorToJob($jobDescriptor, $job);
643
644
        // see if it needs 'setup' or 'restart' called
645
        if ($jobDescriptor->StepsProcessed <= 0) {
646
            $job->setup();
647
        } else {
648
            $job->prepareForRestart();
649
        }
650
651
        // make sure the descriptor is up to date with anything changed
652
        $this->copyJobToDescriptor($job, $jobDescriptor);
653
        $jobDescriptor->write();
654
655
        return $job;
656
    }
657
658
    /**
659
     * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
660
     * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
661
     * This may still have a race condition, but this should minimise the possibility.
662
     * Side effect is the job status will be changed to "Initialised".
663
     *
664
     * Assumption is the job has a status of "Queued" or "Wait".
665
     *
666
     * @param QueuedJobDescriptor $jobDescriptor
667
     *
668
     * @return boolean
669
     */
670
    protected function grabMutex(QueuedJobDescriptor $jobDescriptor)
671
    {
672
        // write the status and determine if any rows were affected, for protection against a
673
        // potential race condition where two or more processes init the same job at once.
674
        // This deliberately does not use write() as that would always update LastEdited
675
        // and thus the row would always be affected.
676
        try {
677
            DB::query(sprintf(
678
                'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s'
679
                . ' AND "JobFinished" IS NULL AND "JobStatus" NOT IN (%s)',
680
                QueuedJob::STATUS_INIT,
681
                $jobDescriptor->ID,
682
                "'" . QueuedJob::STATUS_RUN . "', '" . QueuedJob::STATUS_COMPLETE . "', '"
683
                . QueuedJob::STATUS_PAUSED . "', '" . QueuedJob::STATUS_CANCELLED . "'"
684
            ));
685
        } catch (Exception $e) {
686
            return false;
687
        }
688
689
        if (DB::get_conn()->affectedRows() === 0 && $jobDescriptor->JobStatus !== QueuedJob::STATUS_INIT) {
690
            return false;
691
        }
692
693
        return true;
694
    }
695
696
    /**
697
     * Start the actual execution of a job.
698
     * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
699
     *
700
     * This method will continue executing until the job says it's completed
701
     *
702
     * @param int $jobId
703
     *          The ID of the job to start executing
704
     *
705
     * @return boolean
706
     * @throws Exception
707
     */
708
    public function runJob($jobId)
709
    {
710
        // first retrieve the descriptor
711
        /** @var QueuedJobDescriptor $jobDescriptor */
712
        $jobDescriptor = DataObject::get_by_id(
713
            QueuedJobDescriptor::class,
714
            (int)$jobId
715
        );
716
        if (!$jobDescriptor) {
717
            throw new Exception("$jobId is invalid");
718
        }
719
720
        // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
721
        // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
722
        // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
723
        // as the currently logged in user and hope for the best
724
725
        // We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
726
        // this point of execution in some circumstances
727
        $originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
728
        /** @var Member|null $originalUser */
729
        $originalUser = $originalUserID
730
            ? DataObject::get_by_id(Member::class, $originalUserID)
731
            : null;
732
        $runAsUser = null;
733
734
        // If the Job has requested that we run it as a particular user, then we should try and do that.
735
        if ($jobDescriptor->RunAs() !== null) {
736
            $runAsUser = $this->setRunAsUser($jobDescriptor->RunAs(), $originalUser);
737
        }
738
739
        // set up a custom error handler for this processing
740
        $errorHandler = new JobErrorHandler();
741
742
        $job = null;
743
744
        $broken = false;
745
746
        // Push a config context onto the stack for the duration of this job run.
747
        Config::nest();
748
749
        if ($this->grabMutex($jobDescriptor)) {
750
            try {
751
                $job = $this->initialiseJob($jobDescriptor);
752
753
                // get the job ready to begin.
754
                if (!$jobDescriptor->JobStarted) {
755
                    $jobDescriptor->JobStarted = DBDatetime::now()->Rfc2822();
756
                } else {
757
                    $jobDescriptor->JobRestarted = DBDatetime::now()->Rfc2822();
0 ignored issues
show
Documentation introduced by
The property JobRestarted 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...
758
                }
759
760
                // Only write to job as "Running" if 'isComplete' was NOT set to true
761
                // during setup() or prepareForRestart()
762
                if (!$job->jobFinished()) {
763
                    $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
764
                    $jobDescriptor->write();
765
                }
766
767
                $lastStepProcessed = 0;
768
                // have we stalled at all?
769
                $stallCount = 0;
770
771
                if (class_exists(Subsite::class) && !empty($job->SubsiteID)) {
772
                    Subsite::changeSubsite($job->SubsiteID);
773
774
                    // lets set the base URL as far as Director is concerned so that our URLs are correct
775
                    /** @var Subsite $subsite */
776
                    $subsite = DataObject::get_by_id(Subsite::class, $job->SubsiteID);
777
                    if ($subsite && $subsite->exists()) {
778
                        $domain = $subsite->domain();
779
                        $base = rtrim(Director::protocol() . $domain, '/') . '/';
780
781
                        Config::modify()->set(Director::class, 'alternate_base_url', $base);
782
                    }
783
                }
784
785
                // while not finished
786
                while (!$job->jobFinished() && !$broken) {
787
                    // see that we haven't been set to 'paused' or otherwise by another process
788
                    /** @var QueuedJobDescriptor $jobDescriptor */
789
                    $jobDescriptor = DataObject::get_by_id(
790
                        QueuedJobDescriptor::class,
791
                        (int)$jobId
792
                    );
793
                    if (!$jobDescriptor || !$jobDescriptor->exists()) {
794
                        $broken = true;
795
                        $this->getLogger()->error(
796
                            print_r(
797
                                [
798
                                    'errno' => 0,
799
                                    'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
800
                                    'errfile' => __FILE__,
801
                                    'errline' => __LINE__,
802
                                    'errcontext' => [],
803
                                ],
804
                                true
805
                            ),
806
                            [
807
                                'file' => __FILE__,
808
                                'line' => __LINE__,
809
                            ]
810
                        );
811
                        break;
812
                    }
813 View Code Duplication
                    if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
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...
814
                        // we've been paused by something, so we'll just exit
815
                        $job->addMessage(_t(
816
                            __CLASS__ . '.JOB_PAUSED',
817
                            'Job paused at {time}',
818
                            ['time' => DBDatetime::now()->Rfc2822()]
819
                        ));
820
                        $broken = true;
821
                    }
822
823
                    if (!$broken) {
824
                        // Inject real-time log handler
825
                        $logger = Injector::inst()->get(LoggerInterface::class);
826
                        if ($logger instanceof Logger) {
827
                            // Check if there is already a handler
828
                            $exists = false;
829
                            foreach ($logger->getHandlers() as $handler) {
830
                                if ($handler instanceof QueuedJobHandler) {
831
                                    $exists = true;
832
                                    break;
833
                                }
834
                            }
835
836
                            if (!$exists) {
837
                                // Add the handler
838
                                /** @var QueuedJobHandler $queuedJobHandler */
839
                                $queuedJobHandler = QueuedJobHandler::create($job, $jobDescriptor);
840
841
                                // We only write for every 100 file
842
                                $bufferHandler = new BufferHandler(
843
                                    $queuedJobHandler,
844
                                    100,
845
                                    Logger::DEBUG,
846
                                    true,
847
                                    true
848
                                );
849
850
                                $logger->pushHandler($bufferHandler);
851
                            }
852
                        } else {
853
                            if ($logger instanceof LoggerInterface) {
854
                                $logger->warning(
855
                                    'Monolog not found, messages will not output while the job is running'
856
                                );
857
                            }
858
                        }
859
860
                        // Collect output as job messages as well as sending it to the screen after processing
861
                        $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...
862
                            $job->addMessage($buffer);
863
                            if ($jobDescriptor) {
864
                                $this->copyJobToDescriptor($job, $jobDescriptor);
865
                                $jobDescriptor->write();
866
                            }
867
                            return $buffer;
868
                        };
869
                        ob_start($obLogger, 256);
870
871
                        try {
872
                            $job->process();
873
                        } catch (Exception $e) {
874
                            // okay, we'll just catch this exception for now
875
                            $job->addMessage(
876
                                _t(
877
                                    __CLASS__ . '.JOB_EXCEPT',
878
                                    'Job caused exception {message} in {file} at line {line}',
879
                                    [
880
                                        'message' => $e->getMessage(),
881
                                        'file' => $e->getFile(),
882
                                        'line' => $e->getLine(),
883
                                    ]
884
                                )
885
                            );
886
                            $this->getLogger()->error(
887
                                $e->getMessage(),
888
                                [
889
                                    'exception' => $e,
890
                                ]
891
                            );
892
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
893
                            $this->extend('updateJobDescriptorAndJobOnException', $jobDescriptor, $job, $e);
894
                        }
895
896
                        // Write any remaining batched messages at the end
897
                        if (isset($bufferHandler)) {
898
                            $bufferHandler->flush();
899
                        }
900
901
                        ob_end_flush();
902
903
                        // now check the job state
904
                        $data = $job->getJobData();
905
                        if ($data->currentStep == $lastStepProcessed) {
906
                            $stallCount++;
907
                        }
908
909 View Code Duplication
                        if ($stallCount > static::config()->get('stall_threshold')) {
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...
910
                            $broken = true;
911
                            $job->addMessage(
912
                                _t(
913
                                    __CLASS__ . '.JOB_STALLED',
914
                                    'Job stalled after {attempts} attempts - please check',
915
                                    ['attempts' => $stallCount]
916
                                )
917
                            );
918
                            $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN;
919
                        }
920
921
                        // now we'll be good and check our memory usage. If it is too high, we'll set the job to
922
                        // a 'Waiting' state, and let the next processing run pick up the job.
923
                        if ($this->isMemoryTooHigh()) {
924
                            $job->addMessage(
925
                                _t(
926
                                    __CLASS__ . '.MEMORY_RELEASE',
927
                                    'Job releasing memory and waiting ({used} used)',
928
                                    ['used' => $this->humanReadable($this->getMemoryUsage())]
929
                                )
930
                            );
931
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
932
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
933
                            }
934
                            $broken = true;
935
                        }
936
937
                        // Also check if we are running too long
938
                        if ($this->hasPassedTimeLimit()) {
939
                            $job->addMessage(_t(
940
                                __CLASS__ . '.TIME_LIMIT',
941
                                'Queue has passed time limit and will restart before continuing'
942
                            ));
943
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
944
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
945
                            }
946
                            $broken = true;
947
                        }
948
                    }
949
950
                    if ($jobDescriptor) {
951
                        $this->copyJobToDescriptor($job, $jobDescriptor);
952
                        $jobDescriptor->write();
953 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...
954
                        $this->getLogger()->error(
955
                            print_r(
956
                                [
957
                                    'errno' => 0,
958
                                    'errstr' => 'Job descriptor has been set to null',
959
                                    'errfile' => __FILE__,
960
                                    'errline' => __LINE__,
961
                                    'errcontext' => [],
962
                                ],
963
                                true
964
                            ),
965
                            [
966
                                'file' => __FILE__,
967
                                'line' => __LINE__,
968
                            ]
969
                        );
970
                        $broken = true;
971
                    }
972
                }
973
974
                // a last final save. The job is complete by now
975
                if ($jobDescriptor) {
976
                    $jobDescriptor->write();
977
                }
978
979
                if ($job->jobFinished()) {
980
                    /** @var AbstractQueuedJob|QueuedJob $job */
981
                    $job->afterComplete();
982
                    $jobDescriptor->cleanupJob();
983
984
                    $this->extend('updateJobDescriptorAndJobOnCompletion', $jobDescriptor, $job);
985
                }
986
            } catch (Exception $e) {
987
                // PHP 5.6 exception handling
988
                $this->handleBrokenJobException($jobDescriptor, $job, $e);
989
                $broken = true;
990
            } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
991
                // PHP 7 Error handling)
992
                $this->handleBrokenJobException($jobDescriptor, $job, $e);
993
                $broken = true;
994
            }
995
        }
996
997
        $errorHandler->clear();
998
999
        Config::unnest();
1000
1001
        $this->unsetRunAsUser($runAsUser, $originalUser);
1002
1003
        return !$broken;
1004
    }
1005
1006
    /**
1007
     * @param QueuedJobDescriptor $jobDescriptor
1008
     * @param QueuedJob $job
1009
     * @param Exception|\Throwable $e
1010
     */
1011
    protected function handleBrokenJobException(QueuedJobDescriptor $jobDescriptor, QueuedJob $job, $e)
1012
    {
1013
        // okay, we'll just catch this exception for now
1014
        $this->getLogger()->info(
1015
            $e->getMessage(),
1016
            [
1017
                'exception' => $e,
1018
            ]
1019
        );
1020
        $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
1021
        $this->extend('updateJobDescriptorAndJobOnException', $jobDescriptor, $job, $e);
1022
        $jobDescriptor->write();
1023
    }
1024
1025
    /**
1026
     * @param Member $runAsUser
1027
     * @param Member|null $originalUser
1028
     * @return null|Member
1029
     */
1030
    protected function setRunAsUser(Member $runAsUser, Member $originalUser = null)
1031
    {
1032
        // Sanity check. Can't set the user if they don't exist.
1033
        if ($runAsUser === null || !$runAsUser->exists()) {
1034
            return null;
1035
        }
1036
1037
        // Don't need to set Security user if we're already logged in as that same user.
1038
        if ($originalUser && $originalUser->ID === $runAsUser->ID) {
1039
            return null;
1040
        }
1041
1042
        // We are currently either not logged in at all, or we're logged in as a different user. We should switch users
1043
        // so that the context within the Job is correct.
1044
        if (Controller::has_curr()) {
1045
            Security::setCurrentUser($runAsUser);
1046
        } else {
1047
            $_SESSION['loggedInAs'] = $runAsUser->ID;
1048
        }
1049
1050
        // This is an explicit coupling brought about by SS not having a nice way of mocking a user, as it requires
1051
        // session nastiness
1052
        if (class_exists('SecurityContext')) {
1053
            singleton('SecurityContext')->setMember($runAsUser);
1054
        }
1055
1056
        return $runAsUser;
1057
    }
1058
1059
    /**
1060
     * @param Member|null $runAsUser
1061
     * @param Member|null $originalUser
1062
     */
1063
    protected function unsetRunAsUser(Member $runAsUser = null, Member $originalUser = null)
1064
    {
1065
        // No runAsUser was set, so we don't need to do anything.
1066
        if ($runAsUser === null) {
1067
            return;
1068
        }
1069
1070
        // There was no originalUser, so we should make sure that we set the user back to null.
1071
        if (!$originalUser) {
1072
            if (Controller::has_curr()) {
1073
                Security::setCurrentUser(null);
1074
            } else {
1075
                $_SESSION['loggedInAs'] = null;
1076
            }
1077
1078
            return;
1079
        }
1080
1081
        // Okay let's reset our user.
1082
        if (Controller::has_curr()) {
1083
            Security::setCurrentUser($originalUser);
1084
        } else {
1085
            $_SESSION['loggedInAs'] = $originalUser->ID;
1086
        }
1087
    }
1088
1089
    /**
1090
     * Start timer
1091
     */
1092
    protected function markStarted()
1093
    {
1094
        if (!$this->startedAt) {
1095
            $this->startedAt = DBDatetime::now()->getTimestamp();
1096
        }
1097
    }
1098
1099
    /**
1100
     * Is execution time too long?
1101
     *
1102
     * @return bool True if the script has passed the configured time_limit
1103
     */
1104
    protected function hasPassedTimeLimit()
1105
    {
1106
        // Ensure a limit exists
1107
        $limit = static::config()->get('time_limit');
1108
        if (!$limit) {
1109
            return false;
1110
        }
1111
1112
        // Ensure started date is set
1113
        $this->markStarted();
1114
1115
        // Check duration
1116
        $now = DBDatetime::now()->getTimestamp();
1117
        return $now > $this->startedAt + $limit;
1118
    }
1119
1120
    /**
1121
     * Is memory usage too high?
1122
     *
1123
     * @return bool
1124
     */
1125
    protected function isMemoryTooHigh()
1126
    {
1127
        $used = $this->getMemoryUsage();
1128
        $limit = $this->getMemoryLimit();
1129
        return $limit && ($used > $limit);
1130
    }
1131
1132
    /**
1133
     * Get peak memory usage of this application
1134
     *
1135
     * @return float
1136
     */
1137
    protected function getMemoryUsage()
1138
    {
1139
        // Note we use real_usage = false
1140
        // http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
1141
        // Also we use the safer peak memory usage
1142
        return (float)memory_get_peak_usage(false);
1143
    }
1144
1145
    /**
1146
     * Determines the memory limit (in bytes) for this application
1147
     * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
1148
     *
1149
     * @return float Memory limit in bytes
1150
     */
1151
    protected function getMemoryLimit()
1152
    {
1153
        // Limit to smaller of explicit limit or php memory limit
1154
        $limit = $this->parseMemory(static::config()->get('memory_limit'));
1155
        if ($limit) {
1156
            return $limit;
1157
        }
1158
1159
        // Fallback to php memory limit
1160
        $phpLimit = $this->getPHPMemoryLimit();
1161
        if ($phpLimit) {
1162
            return $phpLimit;
1163
        }
1164
    }
1165
1166
    /**
1167
     * Calculate the current memory limit of the server
1168
     *
1169
     * @return float
1170
     */
1171
    protected function getPHPMemoryLimit()
1172
    {
1173
        return $this->parseMemory(trim(ini_get("memory_limit")));
1174
    }
1175
1176
    /**
1177
     * Convert memory limit string to bytes.
1178
     * Based on implementation in install.php5
1179
     *
1180
     * @param string $memString
1181
     *
1182
     * @return float
1183
     */
1184
    protected function parseMemory($memString)
1185
    {
1186
        switch (strtolower(substr($memString, -1))) {
1187
            case "b":
1188
                return round(substr($memString, 0, -1));
1189
            case "k":
1190
                return round(substr($memString, 0, -1) * 1024);
1191
            case "m":
1192
                return round(substr($memString, 0, -1) * 1024 * 1024);
1193
            case "g":
1194
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
1195
            default:
1196
                return round($memString);
1197
        }
1198
    }
1199
1200
    protected function humanReadable($size)
1201
    {
1202
        $filesizename = [" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"];
1203
        return $size ? round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
1204
    }
1205
1206
1207
    /**
1208
     * Gets a list of all the current jobs (or jobs that have recently finished)
1209
     *
1210
     * @param string $type
1211
     *          if we're after a particular job list
1212
     * @param int $includeUpUntil
1213
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
1214
     *          includes recently finished jobs
1215
     *
1216
     * @return DataList|QueuedJobDescriptor[]
1217
     */
1218
    public function getJobList($type = null, $includeUpUntil = 0)
1219
    {
1220
        return DataObject::get(
1221
            QueuedJobDescriptor::class,
1222
            $this->getJobListFilter($type, $includeUpUntil)
1223
        );
1224
    }
1225
1226
    /**
1227
     * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
1228
     *
1229
     * @param string $type
1230
     *          if we're after a particular job list
1231
     * @param int $includeUpUntil
1232
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
1233
     *          includes recently finished jobs
1234
     *
1235
     * @return string
1236
     */
1237
    public function getJobListFilter($type = null, $includeUpUntil = 0)
1238
    {
1239
        $util = singleton(QJUtils::class);
1240
1241
        $filter = ['JobStatus <>' => QueuedJob::STATUS_COMPLETE];
1242
        if ($includeUpUntil) {
1243
            $filter['JobFinished > '] = DBDatetime::create()->setValue(
1244
                DBDatetime::now()->getTimestamp() - $includeUpUntil
1245
            )->Rfc2822();
1246
        }
1247
1248
        $filter = $util->dbQuote($filter, ' OR ');
1249
1250
        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...
1251
            $filter = $util->dbQuote(['JobType =' => (string)$type]) . ' AND (' . $filter . ')';
1252
        }
1253
1254
        return $filter;
1255
    }
1256
1257
    /**
1258
     * Process the job queue with the current queue runner
1259
     *
1260
     * @param string $queue
1261
     */
1262
    public function runQueue($queue)
1263
    {
1264
        if (!self::config()->get('disable_health_check')) {
1265
            $this->checkJobHealth($queue);
1266
        }
1267
        $this->checkdefaultJobs($queue);
1268
        $this->queueRunner->runQueue($queue);
1269
    }
1270
1271
    /**
1272
     * Process all jobs from a given queue
1273
     *
1274
     * @param string $name The job queue to completely process
1275
     */
1276
    public function processJobQueue($name)
1277
    {
1278
        // Start timer to measure lifetime
1279
        $this->markStarted();
1280
1281
        // Begin main loop
1282
        do {
1283
            if (class_exists(Subsite::class)) {
1284
                // clear subsite back to default to prevent any subsite changes from leaking to
1285
                // subsequent actions
1286
                Subsite::changeSubsite(0);
1287
            }
1288
1289
            $job = $this->getNextPendingJob($name);
1290
            if ($job) {
1291
                $success = $this->runJob($job->ID);
1292
                if (!$success) {
1293
                    // make sure job is null so it doesn't continue the current
1294
                    // processing loop. Next queue executor can pick up where
1295
                    // things left off
1296
                    $job = null;
1297
                }
1298
            }
1299
        } while ($job);
1300
    }
1301
1302
    /**
1303
     * When PHP shuts down, we want to process all of the immediate queue items
1304
     *
1305
     * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
1306
     * we ignore paused or stalled jobs.
1307
     */
1308
    public function onShutdown()
1309
    {
1310
        $this->processJobQueue(QueuedJob::IMMEDIATE);
1311
    }
1312
1313
    /**
1314
     * Get a logger
1315
     *
1316
     * @return LoggerInterface
1317
     */
1318
    public function getLogger()
1319
    {
1320
        return Injector::inst()->get(LoggerInterface::class);
1321
    }
1322
1323 View Code Duplication
    public static function enableMaintenanceLock(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1324
    {
1325
        if (!static::config()->get('lock_file_enabled')) {
1326
            return;
1327
        }
1328
1329
        $path = static::lockFilePath();
0 ignored issues
show
Bug introduced by
Since lockFilePath() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of lockFilePath() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
1330
        $contents = date('Y-m-d H:i:s');
1331
1332
        file_put_contents($path, $contents);
1333
    }
1334
1335 View Code Duplication
    public static function disableMaintenanceLock(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1336
    {
1337
        if (!static::config()->get('lock_file_enabled')) {
1338
            return;
1339
        }
1340
1341
        $path = static::lockFilePath();
0 ignored issues
show
Bug introduced by
Since lockFilePath() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of lockFilePath() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
1342
        if (!file_exists($path)) {
1343
            return;
1344
        }
1345
1346
        unlink($path);
1347
    }
1348
1349
    /**
1350
     * @return bool
1351
     */
1352
    public static function isMaintenanceLockActive(): bool
1353
    {
1354
        if (!static::config()->get('lock_file_enabled')) {
1355
            return false;
1356
        }
1357
1358
        $path = static::lockFilePath();
0 ignored issues
show
Bug introduced by
Since lockFilePath() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of lockFilePath() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
1359
1360
        return file_exists($path);
1361
    }
1362
1363
    /**
1364
     * @return string
1365
     */
1366
    private static function lockFilePath(): string
1367
    {
1368
        return sprintf(
1369
            '%s%s/%s',
1370
            Director::baseFolder(),
1371
            static::config()->get('lock_file_path'),
1372
            static::config()->get('lock_file_name')
1373
        );
1374
    }
1375
}
1376