[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/jobqueue/ -> JobRunner.php (source)

   1  <?php
   2  /**
   3   * Job queue runner utility methods
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup JobQueue
  22   */
  23  
  24  /**
  25   * Job queue runner utility methods
  26   *
  27   * @ingroup JobQueue
  28   * @since 1.24
  29   */
  30  class JobRunner {
  31      /** @var callable|null Debug output handler */
  32      protected $debug;
  33  
  34      /**
  35       * @param callable $debug Optional debug output handler
  36       */
  37  	public function setDebugHandler( $debug ) {
  38          $this->debug = $debug;
  39      }
  40  
  41      /**
  42       * Run jobs of the specified number/type for the specified time
  43       *
  44       * The response map has a 'job' field that lists status of each job, including:
  45       *   - type   : the job type
  46       *   - status : ok/failed
  47       *   - error  : any error message string
  48       *   - time   : the job run time in ms
  49       * The response map also has:
  50       *   - backoffs : the (job type => seconds) map of backoff times
  51       *   - elapsed  : the total time spent running tasks in ms
  52       *   - reached  : the reason the script finished, one of (none-ready, job-limit, time-limit)
  53       *
  54       * This method outputs status information only if a debug handler was set.
  55       * Any exceptions are caught and logged, but are not reported as output.
  56       *
  57       * @param array $options Map of parameters:
  58       *    - type     : the job type (or false for the default types)
  59       *    - maxJobs  : maximum number of jobs to run
  60       *    - maxTime  : maximum time in seconds before stopping
  61       *    - throttle : whether to respect job backoff configuration
  62       * @return array Summary response that can easily be JSON serialized
  63       */
  64  	public function run( array $options ) {
  65          $response = array( 'jobs' => array(), 'reached' => 'none-ready' );
  66  
  67          $type = isset( $options['type'] ) ? $options['type'] : false;
  68          $maxJobs = isset( $options['maxJobs'] ) ? $options['maxJobs'] : false;
  69          $maxTime = isset( $options['maxTime'] ) ? $options['maxTime'] : false;
  70          $noThrottle = isset( $options['throttle'] ) && !$options['throttle'];
  71  
  72          $group = JobQueueGroup::singleton();
  73          // Handle any required periodic queue maintenance
  74          $count = $group->executeReadyPeriodicTasks();
  75          if ( $count > 0 ) {
  76              $this->runJobsLog( "Executed $count periodic queue task(s)." );
  77          }
  78  
  79          // Flush any pending DB writes for sanity
  80          wfGetLBFactory()->commitMasterChanges();
  81  
  82          // Some jobs types should not run until a certain timestamp
  83          $backoffs = array(); // map of (type => UNIX expiry)
  84          $backoffDeltas = array(); // map of (type => seconds)
  85          $wait = 'wait'; // block to read backoffs the first time
  86  
  87          $jobsRun = 0;
  88          $timeMsTotal = 0;
  89          $flags = JobQueueGroup::USE_CACHE;
  90          $startTime = microtime( true ); // time since jobs started running
  91          $lastTime = microtime( true ); // time since last slave check
  92          do {
  93              // Sync the persistent backoffs with concurrent runners
  94              $backoffs = $this->syncBackoffDeltas( $backoffs, $backoffDeltas, $wait );
  95              $blacklist = $noThrottle ? array() : array_keys( $backoffs );
  96              $wait = 'nowait'; // less important now
  97  
  98              if ( $type === false ) {
  99                  $job = $group->pop( JobQueueGroup::TYPE_DEFAULT, $flags, $blacklist );
 100              } elseif ( in_array( $type, $blacklist ) ) {
 101                  $job = false; // requested queue in backoff state
 102              } else {
 103                  $job = $group->pop( $type ); // job from a single queue
 104              }
 105  
 106              if ( $job ) { // found a job
 107                  $jType = $job->getType();
 108  
 109                  // Back off of certain jobs for a while (for throttling and for errors)
 110                  $ttw = $this->getBackoffTimeToWait( $job );
 111                  if ( $ttw > 0 ) {
 112                      // Always add the delta for other runners in case the time running the
 113                      // job negated the backoff for each individually but not collectively.
 114                      $backoffDeltas[$jType] = isset( $backoffDeltas[$jType] )
 115                          ? $backoffDeltas[$jType] + $ttw
 116                          : $ttw;
 117                      $backoffs = $this->syncBackoffDeltas( $backoffs, $backoffDeltas, $wait );
 118                  }
 119  
 120                  $this->runJobsLog( $job->toString() . " STARTING" );
 121  
 122                  // Run the job...
 123                  wfProfileIn( __METHOD__ . '-' . get_class( $job ) );
 124                  $jobStartTime = microtime( true );
 125                  try {
 126                      ++$jobsRun;
 127                      $status = $job->run();
 128                      $error = $job->getLastError();
 129                      wfGetLBFactory()->commitMasterChanges();
 130                  } catch ( MWException $e ) {
 131                      MWExceptionHandler::rollbackMasterChangesAndLog( $e );
 132                      $status = false;
 133                      $error = get_class( $e ) . ': ' . $e->getMessage();
 134                      MWExceptionHandler::logException( $e );
 135                  }
 136                  $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
 137                  wfProfileOut( __METHOD__ . '-' . get_class( $job ) );
 138                  $timeMsTotal += $timeMs;
 139  
 140                  // Mark the job as done on success or when the job cannot be retried
 141                  if ( $status !== false || !$job->allowRetries() ) {
 142                      $group->ack( $job ); // done
 143                  }
 144  
 145                  // Back off of certain jobs for a while (for throttling and for errors)
 146                  if ( $status === false && mt_rand( 0, 49 ) == 0 ) {
 147                      $ttw = max( $ttw, 30 ); // too many errors
 148                      $backoffDeltas[$jType] = isset( $backoffDeltas[$jType] )
 149                          ? $backoffDeltas[$jType] + $ttw
 150                          : $ttw;
 151                  }
 152  
 153                  if ( $status === false ) {
 154                      $this->runJobsLog( $job->toString() . " t=$timeMs error={$error}" );
 155                  } else {
 156                      $this->runJobsLog( $job->toString() . " t=$timeMs good" );
 157                  }
 158  
 159                  $response['jobs'][] = array(
 160                      'type'   => $jType,
 161                      'status' => ( $status === false ) ? 'failed' : 'ok',
 162                      'error'  => $error,
 163                      'time'   => $timeMs
 164                  );
 165  
 166                  // Break out if we hit the job count or wall time limits...
 167                  if ( $maxJobs && $jobsRun >= $maxJobs ) {
 168                      $response['reached'] = 'job-limit';
 169                      break;
 170                  } elseif ( $maxTime && ( microtime( true ) - $startTime ) > $maxTime ) {
 171                      $response['reached'] = 'time-limit';
 172                      break;
 173                  }
 174  
 175                  // Don't let any of the main DB slaves get backed up
 176                  $timePassed = microtime( true ) - $lastTime;
 177                  if ( $timePassed >= 5 || $timePassed < 0 ) {
 178                      wfWaitForSlaves( $lastTime );
 179                      $lastTime = microtime( true );
 180                  }
 181                  // Don't let any queue slaves/backups fall behind
 182                  if ( $jobsRun > 0 && ( $jobsRun % 100 ) == 0 ) {
 183                      $group->waitForBackups();
 184                  }
 185  
 186                  // Bail if near-OOM instead of in a job
 187                  $this->assertMemoryOK();
 188              }
 189          } while ( $job ); // stop when there are no jobs
 190  
 191          // Sync the persistent backoffs for the next runJobs.php pass
 192          if ( $backoffDeltas ) {
 193              $this->syncBackoffDeltas( $backoffs, $backoffDeltas, 'wait' );
 194          }
 195  
 196          $response['backoffs'] = $backoffs;
 197          $response['elapsed'] = $timeMsTotal;
 198  
 199          return $response;
 200      }
 201  
 202      /**
 203       * @param Job $job
 204       * @return int Seconds for this runner to avoid doing more jobs of this type
 205       * @see $wgJobBackoffThrottling
 206       */
 207  	private function getBackoffTimeToWait( Job $job ) {
 208          global $wgJobBackoffThrottling;
 209  
 210          if ( !isset( $wgJobBackoffThrottling[$job->getType()] ) ||
 211              $job instanceof DuplicateJob // no work was done
 212          ) {
 213              return 0; // not throttled
 214          }
 215  
 216          $itemsPerSecond = $wgJobBackoffThrottling[$job->getType()];
 217          if ( $itemsPerSecond <= 0 ) {
 218              return 0; // not throttled
 219          }
 220  
 221          $seconds = 0;
 222          if ( $job->workItemCount() > 0 ) {
 223              $exactSeconds = $job->workItemCount() / $itemsPerSecond;
 224              // use randomized rounding
 225              $seconds = floor( $exactSeconds );
 226              $remainder = $exactSeconds - $seconds;
 227              $seconds += ( mt_rand() / mt_getrandmax() < $remainder ) ? 1 : 0;
 228          }
 229  
 230          return (int)$seconds;
 231      }
 232  
 233      /**
 234       * Get the previous backoff expiries from persistent storage
 235       * On I/O or lock acquisition failure this returns the original $backoffs.
 236       *
 237       * @param array $backoffs Map of (job type => UNIX timestamp)
 238       * @param string $mode Lock wait mode - "wait" or "nowait"
 239       * @return array Map of (job type => backoff expiry timestamp)
 240       */
 241  	private function loadBackoffs( array $backoffs, $mode = 'wait' ) {
 242          $section = new ProfileSection( __METHOD__ );
 243  
 244          $file = wfTempDir() . '/mw-runJobs-backoffs.json';
 245          if ( is_file( $file ) ) {
 246              $noblock = ( $mode === 'nowait' ) ? LOCK_NB : 0;
 247              $handle = fopen( $file, 'rb' );
 248              if ( !flock( $handle, LOCK_SH | $noblock ) ) {
 249                  fclose( $handle );
 250                  return $backoffs; // don't wait on lock
 251              }
 252              $content = stream_get_contents( $handle );
 253              flock( $handle, LOCK_UN );
 254              fclose( $handle );
 255              $ctime = microtime( true );
 256              $cBackoffs = json_decode( $content, true ) ?: array();
 257              foreach ( $cBackoffs as $type => $timestamp ) {
 258                  if ( $timestamp < $ctime ) {
 259                      unset( $cBackoffs[$type] );
 260                  }
 261              }
 262          } else {
 263              $cBackoffs = array();
 264          }
 265  
 266          return $cBackoffs;
 267      }
 268  
 269      /**
 270       * Merge the current backoff expiries from persistent storage
 271       *
 272       * The $deltas map is set to an empty array on success.
 273       * On I/O or lock acquisition failure this returns the original $backoffs.
 274       *
 275       * @param array $backoffs Map of (job type => UNIX timestamp)
 276       * @param array $deltas Map of (job type => seconds)
 277       * @param string $mode Lock wait mode - "wait" or "nowait"
 278       * @return array The new backoffs account for $backoffs and the latest file data
 279       */
 280  	private function syncBackoffDeltas( array $backoffs, array &$deltas, $mode = 'wait' ) {
 281          $section = new ProfileSection( __METHOD__ );
 282  
 283          if ( !$deltas ) {
 284              return $this->loadBackoffs( $backoffs, $mode );
 285          }
 286  
 287          $noblock = ( $mode === 'nowait' ) ? LOCK_NB : 0;
 288          $file = wfTempDir() . '/mw-runJobs-backoffs.json';
 289          $handle = fopen( $file, 'wb+' );
 290          if ( !flock( $handle, LOCK_EX | $noblock ) ) {
 291              fclose( $handle );
 292              return $backoffs; // don't wait on lock
 293          }
 294          $ctime = microtime( true );
 295          $content = stream_get_contents( $handle );
 296          $cBackoffs = json_decode( $content, true ) ?: array();
 297          foreach ( $deltas as $type => $seconds ) {
 298              $cBackoffs[$type] = isset( $cBackoffs[$type] ) && $cBackoffs[$type] >= $ctime
 299                  ? $cBackoffs[$type] + $seconds
 300                  : $ctime + $seconds;
 301          }
 302          foreach ( $cBackoffs as $type => $timestamp ) {
 303              if ( $timestamp < $ctime ) {
 304                  unset( $cBackoffs[$type] );
 305              }
 306          }
 307          ftruncate( $handle, 0 );
 308          fwrite( $handle, json_encode( $cBackoffs ) );
 309          flock( $handle, LOCK_UN );
 310          fclose( $handle );
 311  
 312          $deltas = array();
 313  
 314          return $cBackoffs;
 315      }
 316  
 317      /**
 318       * Make sure that this script is not too close to the memory usage limit.
 319       * It is better to die in between jobs than OOM right in the middle of one.
 320       * @throws MWException
 321       */
 322  	private function assertMemoryOK() {
 323          static $maxBytes = null;
 324          if ( $maxBytes === null ) {
 325              $m = array();
 326              if ( preg_match( '!^(\d+)(k|m|g|)$!i', ini_get( 'memory_limit' ), $m ) ) {
 327                  list( , $num, $unit ) = $m;
 328                  $conv = array( 'g' => 1073741824, 'm' => 1048576, 'k' => 1024, '' => 1 );
 329                  $maxBytes = $num * $conv[strtolower( $unit )];
 330              } else {
 331                  $maxBytes = 0;
 332              }
 333          }
 334          $usedBytes = memory_get_usage();
 335          if ( $maxBytes && $usedBytes >= 0.95 * $maxBytes ) {
 336              throw new MWException( "Detected excessive memory usage ($usedBytes/$maxBytes)." );
 337          }
 338      }
 339  
 340      /**
 341       * Log the job message
 342       * @param string $msg The message to log
 343       */
 344  	private function runJobsLog( $msg ) {
 345          if ( $this->debug ) {
 346              call_user_func_array( $this->debug, array( wfTimestamp( TS_DB ) . " $msg\n" ) );
 347          }
 348          wfDebugLog( 'runJobs', $msg );
 349      }
 350  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1