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