Index: trunk/phase3/includes/filerepo/backend/FileOp.php |
— | — | @@ -118,7 +118,8 @@ |
119 | 119 | // Restart PHP's execution timer and set the timeout to safe amount. |
120 | 120 | // This handles cases where the operations take a long time or where we are |
121 | 121 | // already running low on time left. The old timeout is restored afterwards. |
122 | | - $scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC ); |
| 122 | + # @TODO: re-enable this for when the number of batches is high. |
| 123 | + #$scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC ); |
123 | 124 | |
124 | 125 | // Attempt each operation... |
125 | 126 | foreach ( $performOps as $index => $fileOp ) { |
— | — | @@ -359,28 +360,52 @@ |
360 | 361 | * the original time limit minus the time the object existed. |
361 | 362 | */ |
362 | 363 | class FileOpScopedPHPTimeout { |
363 | | - protected $startTime; // integer seconds |
364 | | - protected $oldTimeout; // integer seconds |
| 364 | + protected $startTime; // float; seconds |
| 365 | + protected $oldTimeout; // integer; seconds |
365 | 366 | |
| 367 | + protected static $stackDepth = 0; // integer |
| 368 | + protected static $totalCalls = 0; // integer |
| 369 | + protected static $totalElapsed = 0; // float; seconds |
| 370 | + |
| 371 | + /* Prevent callers in infinite loops from running forever */ |
| 372 | + const MAX_TOTAL_CALLS = 1000000; |
| 373 | + const MAX_TOTAL_TIME = 300; // seconds |
| 374 | + |
366 | 375 | /** |
367 | 376 | * @param $seconds integer |
368 | 377 | */ |
369 | 378 | public function __construct( $seconds ) { |
370 | 379 | if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0 |
371 | | - $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); |
| 380 | + if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) { |
| 381 | + trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." ); |
| 382 | + } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) { |
| 383 | + trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." ); |
| 384 | + } elseif ( self::$stackDepth > 0 ) { // recursion guard |
| 385 | + trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." ); |
| 386 | + } else { |
| 387 | + $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); |
| 388 | + $this->startTime = microtime( true ); |
| 389 | + ++self::$stackDepth; |
| 390 | + ++self::$totalCalls; // proof against < 1us scopes |
| 391 | + } |
372 | 392 | } |
373 | | - $this->startTime = time(); |
374 | 393 | } |
375 | 394 | |
376 | | - /* |
| 395 | + /** |
377 | 396 | * Restore the original timeout. |
378 | 397 | * This does not account for the timer value on __construct(). |
379 | 398 | */ |
380 | 399 | public function __destruct() { |
381 | 400 | if ( $this->oldTimeout ) { |
382 | | - $elapsed = time() - $this->startTime; |
| 401 | + $elapsed = microtime( true ) - $this->startTime; |
383 | 402 | // Note: a limit of 0 is treated as "forever" |
384 | | - set_time_limit( max( 1, $this->oldTimeout - $elapsed ) ); |
| 403 | + set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) ); |
| 404 | + // If each scoped timeout is for less than one second, we end up |
| 405 | + // restoring the original timeout without any decrease in value. |
| 406 | + // Thus web scripts in an infinite loop can run forever unless we |
| 407 | + // take some measures to prevent this. Track total time and calls. |
| 408 | + self::$totalElapsed += $elapsed; |
| 409 | + --self::$stackDepth; |
385 | 410 | } |
386 | 411 | } |
387 | 412 | } |