Skip to content

Commit 05defbc

Browse files
committed
Clean up and stabilize timeout detection
1 parent 0ae8b92 commit 05defbc

File tree

1 file changed

+57
-37
lines changed

1 file changed

+57
-37
lines changed

src/AsyncTask.php

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,6 @@ class AsyncTask
4747
*/
4848
private const LARAVEL_START = "LARAVEL_START";
4949

50-
/**
51-
* The time epsilon that will be added to timeout checks to ensure we can correctly handle Unix timeouts.
52-
*
53-
* This is a workaround to the undetectable but inevitable Unix PHP startup delay.
54-
* @var float
55-
*/
56-
private const TIME_EPSILON = 0.1;
57-
5850
/**
5951
* Indicates whether GNU coreutils is found in the system; in particular, we are looking for the timeout command inside coreutils.
6052
*
@@ -101,7 +93,7 @@ public function run(): void
10193

10294
// install a timeout detector
10395
// this single function checks all kinds of timeouts
104-
register_shutdown_function([$this, 'checkTaskTimeout']);
96+
register_shutdown_function([$this, 'shutdownCheckTaskTimeout']);
10597
if (OsInfo::isWindows()) {
10698
// windows can just use PHP's time limit
10799
set_time_limit($this->timeLimit);
@@ -257,45 +249,73 @@ private function pcntlGracefulExit(): never
257249
}
258250

259251
/**
260-
* Checks whether the task timed out, and if so, triggers the timeout handler.
252+
* A shutdown function.
261253
*
262-
* This will check various kinds of timeouts.
263-
*
264-
* This handles Windows timeouts.
254+
* Upon shutdown, checks whether this is due to the task timing out, and if so, triggers the timeout handler.
265255
* @return void
266256
*/
267-
protected function checkTaskTimeout(): void
257+
protected function shutdownCheckTaskTimeout(): void
258+
{
259+
if (!$this->hasTimedOut()) {
260+
// shutdown due to other reasons; skip
261+
return;
262+
}
263+
264+
// timeout!
265+
// trigger the timeout handler
266+
if ($this->theTask instanceof AsyncTaskInterface) {
267+
$this->theTask->handleTimeout();
268+
}
269+
}
270+
271+
/**
272+
* During shutdown, checks whether this shutdown satisfies the "task timed out shutdown" condition.
273+
* @return bool True if this task is timed out according to our specifications.
274+
*/
275+
private function hasTimedOut(): bool
268276
{
269277
// we perform a series of checks to see if this task has timed out
270-
$hasTimedOut = false;
271278

272-
// external killing; could be normal Unix timeout SIG_TERM or manual Windows taskkill
273-
// Laravel Artisan very conveniently has a LARAVEL_START = microtime(true) to let us check time elapsed
279+
// runtime timeout triggers a PHP fatal error
280+
// this can happen on Windows by our specification, or on Unix when the actual CLI PHP time limit is smaller than the time limit of this task
281+
$lastError = error_get_last();
282+
if ($lastError !== null) {
283+
// has fatal error; is it our timeout error?
284+
return str_contains($lastError['message'], "Maximum execution time");
285+
}
286+
unset($lastError);
287+
288+
// not a runtime timeout; one of the following:
289+
// it ended within the time limit; or
290+
// on Unix, it ran out of time so it is getting a SIGTERM from us; or
291+
// it somehow ran out of time, and is being manually detected and killed
274292
if ($this->laravelStartVal !== null) {
275-
// we know when we have started; this can be null when running some test cases
293+
// this is very important; in some test cases, this is being run directly by PHPUnit, and so LARAVEL_START will be null
294+
// in this case, we have no idea when this task has started running, so we cannot deduce any timeout statuses
295+
296+
// check LARAVEL_START with microtime
276297
$timeElapsed = microtime(true) - $this->laravelStartVal;
277-
if ($timeElapsed + self::TIME_EPSILON >= $this->timeLimit) {
278-
// timeout!
279-
$hasTimedOut = true;
298+
if ($timeElapsed >= $this->timeLimit) {
299+
// yes
300+
return true;
280301
}
281-
}
282302

283-
// runtime timeout triggers a PHP fatal error
284-
// check this
285-
$lastError = error_get_last();
286-
if ($lastError !== null && str_contains($lastError['message'], "Maximum execution time")) {
287-
// has error, and is timeout!
288-
$hasTimedOut = true;
303+
// if we are on Unix, sometimes LARAVEL_START does not store an accurate task-start time due to slow PHP startup
304+
// and so microtime LARAVEL_START cannot see the timeout
305+
// then, we can still use the kernel's proc stats
306+
// this method should be slower than the microtime method
307+
if (OsInfo::isUnix()) {
308+
// whoami
309+
$selfPID = getmypid();
310+
// get time elapsed in seconds
311+
$tempOut = exec("ps -p $selfPID -o etimes=");
312+
// this must exist (we are still running!), otherwise it indicates the kernel is broken and we can go grab a chicken dinner instead
313+
$timeElapsed = (int) $tempOut;
314+
return $timeElapsed >= $this->timeLimit;
315+
}
289316
}
290317

291-
// all checks concluded
292-
if (!$hasTimedOut) {
293-
// not timeout-related
294-
return;
295-
}
296-
// timeout!
297-
if ($this->theTask instanceof AsyncTaskInterface) {
298-
$this->theTask->handleTimeout();
299-
}
318+
// didn't see anything; assume is no
319+
return false;
300320
}
301321
}

0 commit comments

Comments
 (0)