diff --git a/src/Queue/Adapter/Swoole.php b/src/Queue/Adapter/Swoole.php index b6f4a79..bf5c3d8 100644 --- a/src/Queue/Adapter/Swoole.php +++ b/src/Queue/Adapter/Swoole.php @@ -234,6 +234,14 @@ protected function consumeBound( while (!$this->isStopped()) { $slots->push(true); + // The push blocks while every slot is busy, so a stop that landed + // during a handler is first seen here. Receiving now would claim + // a message the process was told not to take. + if ($this->isStopped()) { + $slots->pop(); + break; + } + $message = $this->nextMessage($errorCallback); if (!$message instanceof Message) { @@ -287,6 +295,14 @@ protected function run( while (!$this->isStopped()) { $slots->push(true); + // The push blocks while every slot is busy, so a stop that landed + // during a handler is first seen here. Receiving now would claim + // a message the process was told not to take. + if ($this->isStopped()) { + $slots->pop(); + break; + } + $message = $this->nextMessageFrom($errorCallback, $queue, $consumer); if (!$message instanceof Message) { diff --git a/tests/Queue/E2E/Adapter/SwooleRestartTest.php b/tests/Queue/E2E/Adapter/SwooleRestartTest.php index 60504c1..85b3e2e 100644 --- a/tests/Queue/E2E/Adapter/SwooleRestartTest.php +++ b/tests/Queue/E2E/Adapter/SwooleRestartTest.php @@ -123,10 +123,16 @@ public function testShutdownDrainsJobWithoutRestartingWorkers(int $signal): void $ready = $this->waitFor('ready', 3); $this->events = []; $this->publish(0, 'slow'); + $this->publish(0, 'slow'); $this->waitFor('started', 1); $this->assertTrue(proc_terminate($this->process, $signal)); $this->waitFor('exited', 1); + // The job in flight finishes; the one behind it stays put. Before the + // stop flag was re-checked after the slot came free, the loop went + // straight back to receive and pulled it, and a job accepted after + // SIGTERM is one more the grace period has to cover. $this->assertCount(1, array_filter($this->events, fn(array $e): bool => $e['event'] === 'processed')); + $this->assertSame(1, $this->queued(0), 'A message published behind the in-flight job must still be on the queue after the drain'); $this->assertCount(3, array_filter($this->events, fn(array $e): bool => $e['event'] === 'stopped')); $this->assertCount(0, array_filter($this->events, fn(array $e): bool => $e['event'] === 'ready')); foreach ($ready as $worker) { @@ -154,6 +160,14 @@ private function publish(int $worker, string $mode): void $this->assertTrue($broker->publish(new Queue('worker-' . $worker, $this->namespace), ['mode' => $mode])); } + private function queued(int $worker): int + { + $redis = new \Redis(); + $redis->connect('127.0.0.1', 16379); + + return (int) $redis->lLen($this->namespace . '.queue.worker-' . $worker); + } + private function waitFor(string $event, int $count): array { $deadline = microtime(true) + 10;