@@ -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