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