PageRenderTime 48ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/job/JobQueue.php

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