Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / jobqueue / Job.php
blobd6a909ed4319d7e59f4482aec63ac1e8ef0b9b4c
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.
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.
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
18 * @file
21 use MediaWiki\Http\Telemetry;
22 use MediaWiki\Json\FormatJson;
23 use MediaWiki\MediaWikiServices;
24 use MediaWiki\Page\PageReference;
25 use MediaWiki\Title\Title;
27 /**
28 * Describe and execute a background job.
30 * Callers should use JobQueueGroup to enqueue jobs for deferred execution.
32 * See [the architecture doc](@ref jobqueuearch) for more information.
34 * @stable to extend
35 * @since 1.6
36 * @ingroup JobQueue
38 abstract class Job implements RunnableJob {
39 /** @var string */
40 public $command;
42 /** @var array Array of job parameters */
43 public $params;
45 /** @var array Additional queue metadata */
46 public $metadata = [];
48 /** @var Title */
49 protected $title;
51 /** @var bool Expensive jobs may set this to true */
52 protected $removeDuplicates = false;
54 /** @var string Text for error that occurred last */
55 protected $error;
57 /** @var callable[] */
58 protected $teardownCallbacks = [];
60 /** @var int Bitfield of JOB_* class constants */
61 protected $executionFlags = 0;
63 /**
64 * Create the appropriate object to handle a specific job
66 * @deprecated since 1.40, use JobFactory instead.
68 * @param string $command Job command
69 * @param array|PageReference $params Job parameters
70 * @return Job
72 public static function factory( $command, $params = [] ) {
73 $factory = MediaWikiServices::getInstance()->getJobFactory();
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() );
80 /**
81 * @stable to call
83 * @param string $command
84 * @param array|PageReference|null $params
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;
96 if ( !is_array( $params ) ) {
97 throw new InvalidArgumentException( '$params must be an array' );
100 if (
101 $page &&
102 !isset( $params['namespace'] ) &&
103 !isset( $params['title'] )
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();
111 $this->command = $command;
112 $this->params = $params + [
113 'requestId' => Telemetry::getInstance()->getRequestId(),
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, '' );
127 * @inheritDoc
128 * @stable to override
130 public function hasExecutionFlag( $flag ) {
131 return ( $this->executionFlags & $flag ) === $flag;
135 * @inheritDoc
136 * @stable to override
138 public function getType() {
139 return $this->command;
143 * @return Title
145 final public function getTitle() {
146 return $this->title;
150 * @inheritDoc
151 * @stable to override
153 public function getParams() {
154 return $this->params;
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
163 public function getMetadata( $field = null ) {
164 if ( $field === null ) {
165 return $this->metadata;
168 return $this->metadata[$field] ?? null;
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
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;
186 return $old;
190 * @stable to override
191 * @return int|null UNIX timestamp to delay running this job until, otherwise null
192 * @since 1.22
194 public function getReleaseTimestamp() {
195 $time = wfTimestampOrNull( TS_UNIX, $this->params['jobReleaseTimestamp'] ?? null );
196 return $time ? (int)$time : null;
200 * @return int|null UNIX timestamp of when the job was queued, or null
201 * @since 1.26
203 public function getQueuedTimestamp() {
204 $time = wfTimestampOrNull( TS_UNIX, $this->metadata['timestamp'] ?? null );
205 return $time ? (int)$time : null;
209 * @inheritDoc
210 * @stable to override
212 public function getRequestId() {
213 return $this->params['requestId'] ?? null;
217 * @inheritDoc
218 * @stable to override
220 public function getReadyTimestamp() {
221 return $this->getReleaseTimestamp() ?: $this->getQueuedTimestamp();
225 * Whether the queue should reject insertion of this job if a duplicate exists
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.
233 * @stable to override
235 * @return bool
237 public function ignoreDuplicates() {
238 return $this->removeDuplicates;
242 * @inheritDoc
243 * @stable to override
245 public function allowRetries() {
246 return true;
250 * @stable to override
251 * @return int
253 public function workItemCount() {
254 return 1;
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.
263 * @stable to override
264 * @return array Map of key/values
265 * @since 1.21
267 public function getDeduplicationInfo() {
268 $info = [
269 'type' => $this->getType(),
270 'params' => $this->getParams()
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'] );
284 return $info;
288 * Get "root job" parameters for a task
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.
297 * @see JobQueue::deduplicateRootJob()
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
306 public static function newRootJobParams( $key ) {
307 return [
308 'rootJobIsSelf' => true,
309 'rootJobSignature' => sha1( $key ),
310 'rootJobTimestamp' => wfTimestampNow()
315 * @stable to override
316 * @see JobQueue::deduplicateRootJob()
317 * @return array
318 * @since 1.21
320 public function getRootJobParams() {
321 return [
322 'rootJobSignature' => $this->params['rootJobSignature'] ?? null,
323 'rootJobTimestamp' => $this->params['rootJobTimestamp'] ?? null
328 * @stable to override
329 * @see JobQueue::deduplicateRootJob()
330 * @return bool
331 * @since 1.22
333 public function hasRootJobParams() {
334 return isset( $this->params['rootJobSignature'] )
335 && isset( $this->params['rootJobTimestamp'] );
339 * @stable to override
340 * @see JobQueue::deduplicateRootJob()
341 * @return bool Whether this is job is a root job
343 public function isRootJob() {
344 return $this->hasRootJobParams() && !empty( $this->params['rootJobIsSelf'] );
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
353 protected function addTeardownCallback( $callback ) {
354 $this->teardownCallbacks[] = $callback;
358 * @inheritDoc
359 * @stable to override
361 public function teardown( $status ) {
362 foreach ( $this->teardownCallbacks as $callback ) {
363 call_user_func( $callback, $status );
368 * @inheritDoc
369 * @stable to override
371 public function toString() {
372 $paramString = '';
373 if ( $this->params ) {
374 foreach ( $this->params as $key => $value ) {
375 if ( $paramString != '' ) {
376 $paramString .= ' ';
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;
388 if ( count( $filteredValue ) <= 10 ) {
389 $value = FormatJson::encode( $filteredValue );
390 } else {
391 $value = "array(" . count( $value ) . ")";
393 } elseif ( is_object( $value ) && !method_exists( $value, '__toString' ) ) {
394 $value = get_debug_type( $value );
397 $flatValue = (string)$value;
398 if ( mb_strlen( $flatValue ) > 1024 ) {
399 $flatValue = "string(" . mb_strlen( $value ) . ")";
402 // Remove newline characters from the value, since
403 // newlines indicate new job lines in log files
404 $flatValue = preg_replace( '/\s+/', ' ', $flatValue );
406 $paramString .= "$key={$flatValue}";
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" );
417 $s = $this->command;
418 if ( is_object( $this->title ) ) {
419 $s .= ' ' . $this->title->getPrefixedDBkey();
421 if ( $paramString != '' ) {
422 $s .= " $paramString";
424 if ( $metaString != '' ) {
425 $s .= " ($metaString)";
428 return $s;
431 protected function setLastError( $error ) {
432 $this->error = $error;
436 * @inheritDoc
437 * @stable to override
439 public function getLastError() {
440 return $this->error;