]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/JobQueue.php
MediaWiki 1.15.5
[autoinstallsdev/mediawiki.git] / includes / JobQueue.php
1 <?php
2 /**
3  * @defgroup JobQueue JobQueue
4  */
5
6 if ( !defined( 'MEDIAWIKI' ) ) {
7         die( "This file is part of MediaWiki, it is not a valid entry point\n" );
8 }
9
10 /**
11  * Class to both describe a background job and handle jobs.
12  *
13  * @ingroup JobQueue
14  */
15 abstract class Job {
16         var $command,
17                 $title,
18                 $params,
19                 $id,
20                 $removeDuplicates,
21                 $error;
22
23         /*-------------------------------------------------------------------------
24          * Abstract functions
25          *------------------------------------------------------------------------*/
26
27         /**
28          * Run the job
29          * @return boolean success
30          */
31         abstract function run();
32
33         /*-------------------------------------------------------------------------
34          * Static functions
35          *------------------------------------------------------------------------*/
36
37         /**
38          * @deprecated use LinksUpdate::queueRecursiveJobs()
39          */
40         /**
41          * static function queueLinksJobs( $titles ) {}
42          */
43
44         /**
45          * Pop a job of a certain type.  This tries less hard than pop() to
46          * actually find a job; it may be adversely affected by concurrent job
47          * runners.
48          */
49         static function pop_type($type) {
50                 wfProfilein( __METHOD__ );
51
52                 $dbw = wfGetDB( DB_MASTER );
53
54
55                 $row = $dbw->selectRow( 'job', '*', array( 'job_cmd' => $type ), __METHOD__,
56                                 array( 'LIMIT' => 1 ));
57
58                 if ($row === false) {
59                         wfProfileOut( __METHOD__ );
60                         return false;
61                 }
62
63                 /* Ensure we "own" this row */
64                 $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
65                 $affected = $dbw->affectedRows();
66
67                 if ($affected == 0) {
68                         wfProfileOut( __METHOD__ );
69                         return false;
70                 }
71
72                 $namespace = $row->job_namespace;
73                 $dbkey = $row->job_title;
74                 $title = Title::makeTitleSafe( $namespace, $dbkey );
75                 $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
76
77                 $dbw->delete( 'job', $job->insertFields(), __METHOD__ );
78                 $dbw->immediateCommit();
79
80                 wfProfileOut( __METHOD__ );
81                 return $job;
82         }
83
84         /**
85          * Pop a job off the front of the queue
86          *
87          * @param $offset Number of jobs to skip
88          * @return Job or false if there's no jobs
89          */
90         static function pop($offset=0) {
91                 wfProfileIn( __METHOD__ );
92
93                 $dbr = wfGetDB( DB_SLAVE );
94
95                 /* Get a job from the slave, start with an offset,
96                         scan full set afterwards, avoid hitting purged rows
97
98                         NB: If random fetch previously was used, offset
99                                 will always be ahead of few entries
100                 */
101
102                 $row = $dbr->selectRow( 'job', '*', "job_id >= ${offset}", __METHOD__,
103                         array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ));
104
105                 // Refetching without offset is needed as some of job IDs could have had delayed commits
106                 // and have lower IDs than jobs already executed, blame concurrency :)
107                 //
108                 if ( $row === false) {
109                         if ($offset!=0)
110                                 $row = $dbr->selectRow( 'job', '*', '', __METHOD__,
111                                         array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ));
112
113                         if ($row === false ) {
114                                 wfProfileOut( __METHOD__ );
115                                 return false;
116                         }
117                 }
118                 $offset = $row->job_id;
119
120                 // Try to delete it from the master
121                 $dbw = wfGetDB( DB_MASTER );
122                 $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
123                 $affected = $dbw->affectedRows();
124                 $dbw->immediateCommit();
125
126                 if ( !$affected ) {
127                         // Failed, someone else beat us to it
128                         // Try getting a random row
129                         $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob',
130                                 'MAX(job_id) as maxjob' ), '1=1', __METHOD__ );
131                         if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) {
132                                 // No jobs to get
133                                 wfProfileOut( __METHOD__ );
134                                 return false;
135                         }
136                         // Get the random row
137                         $row = $dbw->selectRow( 'job', '*',
138                                 'job_id >= ' . mt_rand( $row->minjob, $row->maxjob ),   __METHOD__ );
139                         if ( $row === false ) {
140                                 // Random job gone before we got the chance to select it
141                                 // Give up
142                                 wfProfileOut( __METHOD__ );
143                                 return false;
144                         }
145                         // Delete the random row
146                         $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
147                         $affected = $dbw->affectedRows();
148                         $dbw->immediateCommit();
149
150                         if ( !$affected ) {
151                                 // Random job gone before we exclusively deleted it
152                                 // Give up
153                                 wfProfileOut( __METHOD__ );
154                                 return false;
155                         }
156                 }
157
158                 // If execution got to here, there's a row in $row that has been deleted from the database
159                 // by this thread. Hence the concurrent pop was successful.
160                 $namespace = $row->job_namespace;
161                 $dbkey = $row->job_title;
162                 $title = Title::makeTitleSafe( $namespace, $dbkey );
163                 $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
164
165                 // Remove any duplicates it may have later in the queue
166                 // Deadlock prone section
167                 $dbw->begin();
168                 $dbw->delete( 'job', $job->insertFields(), __METHOD__ );
169                 $dbw->commit();
170
171                 wfProfileOut( __METHOD__ );
172                 return $job;
173         }
174
175         /**
176          * Create the appropriate object to handle a specific job
177          *
178          * @param $command String: Job command
179          * @param $title Title: Associated title
180          * @param $params Array: Job parameters
181          * @param $id Int: Job identifier
182          * @return Job
183          */
184         static function factory( $command, $title, $params = false, $id = 0 ) {
185                 global $wgJobClasses;
186                 if( isset( $wgJobClasses[$command] ) ) {
187                         $class = $wgJobClasses[$command];
188                         return new $class( $title, $params, $id );
189                 }
190                 throw new MWException( "Invalid job command `{$command}`" );
191         }
192
193         static function makeBlob( $params ) {
194                 if ( $params !== false ) {
195                         return serialize( $params );
196                 } else {
197                         return '';
198                 }
199         }
200
201         static function extractBlob( $blob ) {
202                 if ( (string)$blob !== '' ) {
203                         return unserialize( $blob );
204                 } else {
205                         return false;
206                 }
207         }
208
209         /**
210          * Batch-insert a group of jobs into the queue.
211          * This will be wrapped in a transaction with a forced commit.
212          *
213          * This may add duplicate at insert time, but they will be
214          * removed later on, when the first one is popped.
215          *
216          * @param $jobs array of Job objects
217          */
218         static function batchInsert( $jobs ) {
219                 if( !count( $jobs ) ) {
220                         return;
221                 }
222                 $dbw = wfGetDB( DB_MASTER );
223                 $rows = array();
224                 foreach( $jobs as $job ) {
225                         $rows[] = $job->insertFields();
226                         if ( count( $rows ) >= 50 ) {
227                                 # Do a small transaction to avoid slave lag
228                                 $dbw->begin();
229                                 $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
230                                 $dbw->commit();
231                                 $rows = array();
232                         }
233                 }
234                 if ( $rows ) {
235                         $dbw->begin();
236                         $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
237                         $dbw->commit();
238                 }
239         }
240
241         /*-------------------------------------------------------------------------
242          * Non-static functions
243          *------------------------------------------------------------------------*/
244
245         function __construct( $command, $title, $params = false, $id = 0 ) {
246                 $this->command = $command;
247                 $this->title = $title;
248                 $this->params = $params;
249                 $this->id = $id;
250
251                 // A bit of premature generalisation
252                 // Oh well, the whole class is premature generalisation really
253                 $this->removeDuplicates = true;
254         }
255
256         /**
257          * Insert a single job into the queue.
258          */
259         function insert() {
260                 $fields = $this->insertFields();
261
262                 $dbw = wfGetDB( DB_MASTER );
263
264                 if ( $this->removeDuplicates ) {
265                         $res = $dbw->select( 'job', array( '1' ), $fields, __METHOD__ );
266                         if ( $dbw->numRows( $res ) ) {
267                                 return;
268                         }
269                 }
270                 $fields['job_id'] = $dbw->nextSequenceValue( 'job_job_id_seq' );
271                 $dbw->insert( 'job', $fields, __METHOD__ );
272         }
273
274         protected function insertFields() {
275                 return array(
276                         'job_cmd' => $this->command,
277                         'job_namespace' => $this->title->getNamespace(),
278                         'job_title' => $this->title->getDBkey(),
279                         'job_params' => Job::makeBlob( $this->params )
280                 );
281         }
282
283         function toString() {
284                 $paramString = '';
285                 if ( $this->params ) {
286                         foreach ( $this->params as $key => $value ) {
287                                 if ( $paramString != '' ) {
288                                         $paramString .= ' ';
289                                 }
290                                 $paramString .= "$key=$value";
291                         }
292                 }
293
294                 if ( is_object( $this->title ) ) {
295                         $s = "{$this->command} " . $this->title->getPrefixedDBkey();
296                         if ( $paramString !== '' ) {
297                                 $s .= ' ' . $paramString;
298                         }
299                         return $s;
300                 } else {
301                         return "{$this->command} $paramString";
302                 }
303         }
304
305         protected function setLastError( $error ) {
306                 $this->error = $error;
307         }
308
309         function getLastError() {
310                 return $this->error;
311         }
312 }