From 2c1130ba3a6322eee3f372c35d30f1664590510b Mon Sep 17 00:00:00 2001 From: Dhrupo Nil Date: Sat, 27 Jun 2026 19:18:23 +0600 Subject: [PATCH] [13.x] Resume sleeping when Sleep is interrupted by a signal Sleep::goodnight() called sleep($seconds) but ignored its return value. PHP's sleep() returns the number of seconds left to sleep when it is interrupted by a signal, and queue workers (e.g. Horizon) routinely deliver pcntl signals. As a result a Sleep::for(...)->seconds() call inside a job could wake up early after being interrupted, having slept less than the requested duration. Keep sleeping with the remaining seconds returned by sleep() until the full duration has elapsed. sleep() returns 0 on success and false on error, both of which end the loop, so an uninterrupted sleep behaves exactly as before. Fixes #56647 --- src/Illuminate/Support/Sleep.php | 9 ++++++++- tests/Support/SleepTest.php | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index 35ef55685a38..0b19563915d4 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -342,7 +342,14 @@ protected function goodnight() while ($while()) { if ($seconds > 0) { - sleep($seconds); + $secondsToSleep = $seconds; + + // sleep() returns the number of seconds left to sleep if it was + // interrupted by a signal, such as those sent by a queue worker, + // so we keep sleeping until the requested duration has elapsed. + while ($secondsToSleep > 0) { + $secondsToSleep = sleep($secondsToSleep); + } $remaining = $remaining->subSeconds($seconds); } diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index 2f6388af9613..6d64ebd64b4b 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -32,6 +32,29 @@ public function testItSleepsForSeconds() $this->assertEqualsWithDelta(1, $end - $start, 0.03); } + public function testItKeepsSleepingWhenInterruptedBySignal() + { + if (! function_exists('pcntl_alarm')) { + $this->markTestSkipped('The pcntl extension is required.'); + } + + pcntl_async_signals(true); + pcntl_signal(SIGALRM, fn () => null); + + // Schedule a signal that will interrupt the sleep after one second. + pcntl_alarm(1); + + $start = microtime(true); + Sleep::for(2)->seconds(); + $elapsed = microtime(true) - $start; + + pcntl_alarm(0); + pcntl_signal(SIGALRM, SIG_DFL); + pcntl_async_signals(false); + + $this->assertGreaterThanOrEqual(2, round($elapsed, 1)); + } + public function testCallbacksMayBeExecutedUsingThen() { $this->assertEquals(123, Sleep::for(1)->milliseconds()->then(fn () => 123));