Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.94% covered (warning)
50.94%
54 / 106
16.00% covered (danger)
16.00%
4 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
Job
50.94% covered (warning)
50.94%
54 / 106
16.00% covered (danger)
16.00%
4 / 25
500.29
0.00% covered (danger)
0.00%
0 / 1
 factory
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
10
 hasExecutionFlag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMetadata
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setMetadata
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getReleaseTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getQueuedTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getRequestId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReadyTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 ignoreDuplicates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allowRetries
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 workItemCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeduplicationInfo
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 newRootJobParams
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getRootJobParams
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 hasRootJobParams
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isRootJob
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 addTeardownCallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 teardown
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 toString
85.29% covered (warning)
85.29%
29 / 34
0.00% covered (danger)
0.00%
0 / 1
20.15
 setLastError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\Http\Telemetry;
22use MediaWiki\Json\FormatJson;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Page\PageReference;
25use MediaWiki\Title\Title;
26
27/**
28 * Describe and execute a background job.
29 *
30 * Callers should use JobQueueGroup to enqueue jobs for deferred execution.
31 *
32 * See [the architecture doc](@ref jobqueuearch) for more information.
33 *
34 * @stable to extend
35 * @since 1.6
36 * @ingroup JobQueue
37 */
38abstract class Job implements RunnableJob {
39    /** @var string */
40    public $command;
41
42    /** @var array Array of job parameters */
43    public $params;
44
45    /** @var array Additional queue metadata */
46    public $metadata = [];
47
48    /** @var Title */
49    protected $title;
50
51    /** @var bool Expensive jobs may set this to true */
52    protected $removeDuplicates = false;
53
54    /** @var string Text for error that occurred last */
55    protected $error;
56
57    /** @var callable[] */
58    protected $teardownCallbacks = [];
59
60    /** @var int Bitfield of JOB_* class constants */
61    protected $executionFlags = 0;
62
63    /**
64     * Create the appropriate object to handle a specific job
65     *
66     * @deprecated since 1.40, use JobFactory instead.
67     *
68     * @param string $command Job command
69     * @param array|PageReference $params Job parameters
70     * @return Job
71     */
72    public static function factory( $command, $params = [] ) {
73        $factory = MediaWikiServices::getInstance()->getJobFactory();
74
75        // FIXME: fix handling for legacy signature!
76        // @phan-suppress-next-line PhanParamTooFewUnpack one argument is known to be present.
77        return $factory->newJob( ...func_get_args() );
78    }
79
80    /**
81     * @stable to call
82     *
83     * @param string $command
84     * @param array|PageReference|null $params
85     */
86    public function __construct( $command, $params = null ) {
87        if ( $params instanceof PageReference ) {
88            // Backwards compatibility for old signature ($command, $title, $params)
89            $page = $params;
90            $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
91        } else {
92            // Newer jobs may choose to not have a top-level title (e.g. GenericParameterJob)
93            $page = null;
94        }
95
96        if ( !is_array( $params ) ) {
97            throw new InvalidArgumentException( '$params must be an array' );
98        }
99
100        if (
101            $page &&
102            !isset( $params['namespace'] ) &&
103            !isset( $params['title'] )
104        ) {
105            // When constructing this class for submitting to the queue,
106            // normalise the $page arg of old job classes as part of $params.
107            $params['namespace'] = $page->getNamespace();
108            $params['title'] = $page->getDBkey();
109        }
110
111        $this->command = $command;
112        $this->params = $params + [
113            'requestId' => Telemetry::getInstance()->getRequestId(),
114        ];
115
116        if ( $this->title === null ) {
117            // Set this field for access via getTitle().
118            $this->title = ( isset( $params['namespace'] ) && isset( $params['title'] ) )
119                ? Title::makeTitle( $params['namespace'], $params['title'] )
120                // GenericParameterJob classes without namespace/title params
121                // should not use getTitle(). Set an invalid title as placeholder.
122                : Title::makeTitle( NS_SPECIAL, '' );
123        }
124    }
125
126    /**
127     * @inheritDoc
128     * @stable to override
129     */
130    public function hasExecutionFlag( $flag ) {
131        return ( $this->executionFlags & $flag ) === $flag;
132    }
133
134    /**
135     * @inheritDoc
136     * @stable to override
137     */
138    public function getType() {
139        return $this->command;
140    }
141
142    /**
143     * @return Title
144     */
145    final public function getTitle() {
146        return $this->title;
147    }
148
149    /**
150     * @inheritDoc
151     * @stable to override
152     */
153    public function getParams() {
154        return $this->params;
155    }
156
157    /**
158     * @stable to override
159     * @param string|null $field Metadata field or null to get all the metadata
160     * @return mixed|null Value; null if missing
161     * @since 1.33
162     */
163    public function getMetadata( $field = null ) {
164        if ( $field === null ) {
165            return $this->metadata;
166        }
167
168        return $this->metadata[$field] ?? null;
169    }
170
171    /**
172     * @stable to override
173     * @param string $field Key name to set the value for
174     * @param mixed $value The value to set the field for
175     * @return mixed|null The prior field value; null if missing
176     * @since 1.33
177     */
178    public function setMetadata( $field, $value ) {
179        $old = $this->getMetadata( $field );
180        if ( $value === null ) {
181            unset( $this->metadata[$field] );
182        } else {
183            $this->metadata[$field] = $value;
184        }
185
186        return $old;
187    }
188
189    /**
190     * @stable to override
191     * @return int|null UNIX timestamp to delay running this job until, otherwise null
192     * @since 1.22
193     */
194    public function getReleaseTimestamp() {
195        $time = wfTimestampOrNull( TS_UNIX, $this->params['jobReleaseTimestamp'] ?? null );
196        return $time ? (int)$time : null;
197    }
198
199    /**
200     * @return int|null UNIX timestamp of when the job was queued, or null
201     * @since 1.26
202     */
203    public function getQueuedTimestamp() {
204        $time = wfTimestampOrNull( TS_UNIX, $this->metadata['timestamp'] ?? null );
205        return $time ? (int)$time : null;
206    }
207
208    /**
209     * @inheritDoc
210     * @stable to override
211     */
212    public function getRequestId() {
213        return $this->params['requestId'] ?? null;
214    }
215
216    /**
217     * @inheritDoc
218     * @stable to override
219     */
220    public function getReadyTimestamp() {
221        return $this->getReleaseTimestamp() ?: $this->getQueuedTimestamp();
222    }
223
224    /**
225     * Whether the queue should reject insertion of this job if a duplicate exists
226     *
227     * This can be used to avoid duplicated effort or combined with delayed jobs to
228     * coalesce updates into larger batches. Claimed jobs are never treated as
229     * duplicates of new jobs, and some queues may allow a few duplicates due to
230     * network partitions and fail-over. Thus, additional locking is needed to
231     * enforce mutual exclusion if this is really needed.
232     *
233     * @stable to override
234     *
235     * @return bool
236     */
237    public function ignoreDuplicates() {
238        return $this->removeDuplicates;
239    }
240
241    /**
242     * @inheritDoc
243     * @stable to override
244     */
245    public function allowRetries() {
246        return true;
247    }
248
249    /**
250     * @stable to override
251     * @return int
252     */
253    public function workItemCount() {
254        return 1;
255    }
256
257    /**
258     * Subclasses may need to override this to make duplication detection work.
259     * The resulting map conveys everything that makes the job unique. This is
260     * only checked if ignoreDuplicates() returns true, meaning that duplicate
261     * jobs are supposed to be ignored.
262     *
263     * @stable to override
264     * @return array Map of key/values
265     * @since 1.21
266     */
267    public function getDeduplicationInfo() {
268        $info = [
269            'type' => $this->getType(),
270            'params' => $this->getParams()
271        ];
272        if ( is_array( $info['params'] ) ) {
273            // Identical jobs with different "root" jobs should count as duplicates
274            unset( $info['params']['rootJobSignature'] );
275            unset( $info['params']['rootJobTimestamp'] );
276            // Likewise for jobs with different delay times
277            unset( $info['params']['jobReleaseTimestamp'] );
278            // Identical jobs from different requests should count as duplicates
279            unset( $info['params']['requestId'] );
280            // Queues pack and hash this array, so normalize the order
281            ksort( $info['params'] );
282        }
283
284        return $info;
285    }
286
287    /**
288     * Get "root job" parameters for a task
289     *
290     * This is used to no-op redundant jobs, including child jobs of jobs,
291     * as long as the children inherit the root job parameters. When a job
292     * with root job parameters and "rootJobIsSelf" set is pushed, the
293     * deduplicateRootJob() method is automatically called on it. If the
294     * root job is only virtual and not actually pushed (e.g. the sub-jobs
295     * are inserted directly), then call deduplicateRootJob() directly.
296     *
297     * @see JobQueue::deduplicateRootJob()
298     *
299     * @param string $key A key that identifies the task
300     * @return array Map of:
301     *   - rootJobIsSelf    : true
302     *   - rootJobSignature : hash (e.g. SHA1) that identifies the task
303     *   - rootJobTimestamp : TS_MW timestamp of this instance of the task
304     * @since 1.21
305     */
306    public static function newRootJobParams( $key ) {
307        return [
308            'rootJobIsSelf'    => true,
309            'rootJobSignature' => sha1( $key ),
310            'rootJobTimestamp' => wfTimestampNow()
311        ];
312    }
313
314    /**
315     * @stable to override
316     * @see JobQueue::deduplicateRootJob()
317     * @return array
318     * @since 1.21
319     */
320    public function getRootJobParams() {
321        return [
322            'rootJobSignature' => $this->params['rootJobSignature'] ?? null,
323            'rootJobTimestamp' => $this->params['rootJobTimestamp'] ?? null
324        ];
325    }
326
327    /**
328     * @stable to override
329     * @see JobQueue::deduplicateRootJob()
330     * @return bool
331     * @since 1.22
332     */
333    public function hasRootJobParams() {
334        return isset( $this->params['rootJobSignature'] )
335            && isset( $this->params['rootJobTimestamp'] );
336    }
337
338    /**
339     * @stable to override
340     * @see JobQueue::deduplicateRootJob()
341     * @return bool Whether this is job is a root job
342     */
343    public function isRootJob() {
344        return $this->hasRootJobParams() && !empty( $this->params['rootJobIsSelf'] );
345    }
346
347    /**
348     * @param callable $callback A function with one parameter, the success status, which will be
349     *   false if the job failed or it succeeded but the DB changes could not be committed or
350     *   any deferred updates threw an exception. (This parameter was added in 1.28.)
351     * @since 1.27
352     */
353    protected function addTeardownCallback( $callback ) {
354        $this->teardownCallbacks[] = $callback;
355    }
356
357    /**
358     * @inheritDoc
359     * @stable to override
360     */
361    public function teardown( $status ) {
362        foreach ( $this->teardownCallbacks as $callback ) {
363            call_user_func( $callback, $status );
364        }
365    }
366
367    /**
368     * @inheritDoc
369     * @stable to override
370     */
371    public function toString() {
372        $paramString = '';
373        if ( $this->params ) {
374            foreach ( $this->params as $key => $value ) {
375                if ( $paramString != '' ) {
376                    $paramString .= ' ';
377                }
378                if ( is_array( $value ) ) {
379                    $filteredValue = [];
380                    foreach ( $value as $k => $v ) {
381                        $json = FormatJson::encode( $v );
382                        if ( $json === false || mb_strlen( $json ) > 512 ) {
383                            $filteredValue[$k] = get_debug_type( $v ) . '(...)';
384                        } else {
385                            $filteredValue[$k] = $v;
386                        }
387                    }
388                    if ( count( $filteredValue ) <= 10 ) {
389                        $value = FormatJson::encode( $filteredValue );
390                    } else {
391                        $value = "array(" . count( $value ) . ")";
392                    }
393                } elseif ( is_object( $value ) && !method_exists( $value, '__toString' ) ) {
394                    $value = get_debug_type( $value );
395                }
396
397                $flatValue = (string)$value;
398                if ( mb_strlen( $flatValue ) > 1024 ) {
399                    $flatValue = "string(" . mb_strlen( $value ) . ")";
400                }
401
402                // Remove newline characters from the value, since
403                // newlines indicate new job lines in log files
404                $flatValue = preg_replace( '/\s+/', ' ', $flatValue );
405
406                $paramString .= "$key={$flatValue}";
407            }
408        }
409
410        $metaString = '';
411        foreach ( $this->metadata as $key => $value ) {
412            if ( is_scalar( $value ) && mb_strlen( $value ) < 1024 ) {
413                $metaString .= ( $metaString ? ",$key=$value" : "$key=$value" );
414            }
415        }
416
417        $s = $this->command;
418        if ( is_object( $this->title ) ) {
419            $s .= ' ' . $this->title->getPrefixedDBkey();
420        }
421        if ( $paramString != '' ) {
422            $s .= " $paramString";
423        }
424        if ( $metaString != '' ) {
425            $s .= " ($metaString)";
426        }
427
428        return $s;
429    }
430
431    protected function setLastError( $error ) {
432        $this->error = $error;
433    }
434
435    /**
436     * @inheritDoc
437     * @stable to override
438     */
439    public function getLastError() {
440        return $this->error;
441    }
442}