Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Messenger] Messenger: add --duration option to messenger:stop-workers command #60517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.4
---

* Add `--duration` option to `messenger:stop-workers` command to keep workers in paused state

7.3
---

Expand Down
22 changes: 20 additions & 2 deletions src/Symfony/Component/Messenger/Command/StopWorkersCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
Expand All @@ -35,7 +37,9 @@ public function __construct(
protected function configure(): void
{
$this
->setDefinition([])
->setDefinition([
new InputOption('duration', 'd', InputOption::VALUE_REQUIRED, 'Duration in seconds during which workers are paused (not processing messages)'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</info> command sends a signal to stop any <info>messenger:consume</info> processes that are running.

Expand All @@ -44,6 +48,11 @@ protected function configure(): void
Each worker command will finish the message they are currently processing
and then exit. Worker commands are *not* automatically restarted: that
should be handled by a process control system.

Use the <comment>--duration</comment> option to keep the workers in a paused state (not processing messages) for the given duration (in seconds).
During this time, no messages will be handled, and the workers will not resume until the pause period has passed:

<info>php %command.full_name% --duration=60</info>
EOF
)
;
Expand All @@ -53,11 +62,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);

if (null !== $duration = $input->getOption('duration')) {
if (!is_numeric($duration) || 0 >= $duration) {
throw new InvalidOptionException(\sprintf('Option "duration" must be a positive integer, "%s" passed.', $duration));
}
}

$cacheItem = $this->restartSignalCachePool->getItem(StopWorkerOnRestartSignalListener::RESTART_REQUESTED_TIMESTAMP_KEY);
$cacheItem->set(microtime(true));
$cacheItem->set(microtime(true) + $duration);
$this->restartSignalCachePool->save($cacheItem);

$io->success('Signal successfully sent to stop any running workers.');
if ($duration > 0) {
$io->info(sprintf('Workers will be paused for %s seconds.', $duration));
}

return 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ public function __construct(
) {
}

public function onWorkerStarted(): void
public function onWorkerStarted(WorkerStartedEvent $event): void
{
$this->workerStartedAt = microtime(true);

if ($this->shouldRestart()) {
$event->getWorker()->stop();
$remainingStopSeconds = ceil($this->getEndOfStopTime()) - time();
$this->logger?->info(sprintf('The worker is paused and message processing will resume in %d seconds.', $remainingStopSeconds));
}
}

public function onWorkerRunning(WorkerRunningEvent $event): void
Expand All @@ -54,14 +60,19 @@ public static function getSubscribedEvents(): array
}

private function shouldRestart(): bool
{
return $this->workerStartedAt < $this->getEndOfStopTime();
}

private function getEndOfStopTime(): ?float
{
$cacheItem = $this->cachePool->getItem(self::RESTART_REQUESTED_TIMESTAMP_KEY);

if (!$cacheItem->isHit()) {
// no restart has ever been scheduled
return false;
return null;
}

return $this->workerStartedAt < $cacheItem->get();
return (float) $cacheItem->get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,21 @@ public function testItSetsCacheItem()
$tester = new CommandTester($command);
$tester->execute([]);
}

/**
* @group time-sensitive
*/
public function testItSetsCacheItemWithDurationAdded()
{
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->expects($this->once())->method('set')->with($this->equalToWithDelta(microtime(true), 62)); // "extra" 2 seconds
$cachePool->expects($this->once())->method('getItem')->willReturn($cacheItem);
$cachePool->expects($this->once())->method('save')->with($cacheItem);

$command = new StopWorkersCommand($cachePool);

$tester = new CommandTester($command);
$tester->execute(['--duration' => 60]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener;
use Symfony\Component\Messenger\Worker;

Expand All @@ -26,44 +28,87 @@ class StopWorkerOnRestartSignalListenerTest extends TestCase
/**
* @dataProvider restartTimeProvider
*/
public function testWorkerStopsWhenMemoryLimitExceeded(?int $lastRestartTimeOffset, bool $shouldStop)
public function testWorkerStopsOnStartIfRestartInCache(?int $lastRestartTimeOffset, bool $shouldStop)
{
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->expects($this->once())->method('isHit')->willReturn(true);
$cacheItem->expects($this->once())->method('get')->willReturn(null === $lastRestartTimeOffset ? null : time() + $lastRestartTimeOffset);
$cachePool->expects($this->once())->method('getItem')->willReturn($cacheItem);
$cachePool = $this->createRestartInCachePool($lastRestartTimeOffset);

$worker = $this->createMock(Worker::class);
$worker->expects($shouldStop ? $this->once() : $this->never())->method('stop');
$event = new WorkerRunningEvent($worker, false);
$workerStartedEvent = new WorkerStartedEvent($worker);

$stopOnSignalListener = new StopWorkerOnRestartSignalListener($cachePool);
$stopOnSignalListener->onWorkerStarted();
$stopOnSignalListener->onWorkerRunning($event);
$stopOnSignalListener->onWorkerStarted($workerStartedEvent);
}

public static function restartTimeProvider()
/**
* @dataProvider restartTimeProvider
*/
public function testWorkerStopsIfRestartInCache(?int $lastRestartTimeOffset, bool $shouldStop)
{
$cachePool = $this->createRestartInCachePool($lastRestartTimeOffset);

$worker = $this->createMock(Worker::class);
$worker->expects($shouldStop ? $this->atLeast(1) : $this->never())->method('stop');
$workerStartedEvent = new WorkerStartedEvent($worker);
$workerRunningEvent = new WorkerRunningEvent($worker, false);

$stopOnSignalListener = new StopWorkerOnRestartSignalListener($cachePool);
$stopOnSignalListener->onWorkerStarted($workerStartedEvent);
$stopOnSignalListener->onWorkerRunning($workerRunningEvent);
}

public static function restartTimeProvider(): iterable
{
yield [null, false]; // no cached restart time, do not restart
yield [+10, true]; // 10 seconds after starting, a restart was requested
yield [-10, false]; // a restart was requested, but 10 seconds before we started
}

public function testWorkerDoesNotStopOnStartIfRestartNotInCache()
{
$cachePool = $this->createRestartNotInCachePool();

$worker = $this->createMock(Worker::class);
$worker->expects($this->never())->method('stop');
$workerStartedEvent = new WorkerStartedEvent($worker);

$stopOnSignalListener = new StopWorkerOnRestartSignalListener($cachePool);
$stopOnSignalListener->onWorkerStarted($workerStartedEvent);
}

public function testWorkerDoesNotStopIfRestartNotInCache()
{
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->expects($this->once())->method('isHit')->willReturn(false);
$cacheItem->expects($this->never())->method('get');
$cachePool->expects($this->once())->method('getItem')->willReturn($cacheItem);
$cachePool = $this->createRestartNotInCachePool();

$worker = $this->createMock(Worker::class);
$worker->expects($this->never())->method('stop');
$event = new WorkerRunningEvent($worker, false);
$workerStartedEvent = new WorkerStartedEvent($worker);
$workerRunningEvent = new WorkerRunningEvent($worker, false);

$stopOnSignalListener = new StopWorkerOnRestartSignalListener($cachePool);
$stopOnSignalListener->onWorkerStarted();
$stopOnSignalListener->onWorkerRunning($event);
$stopOnSignalListener->onWorkerStarted($workerStartedEvent);
$stopOnSignalListener->onWorkerRunning($workerRunningEvent);
}

private function createRestartInCachePool(?int $value): CacheItemPoolInterface
{
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('isHit')->willReturn(true);
$cacheItem->method('get')->willReturn(null === $value ? null : time() + $value);
$cachePool->method('getItem')->willReturn($cacheItem);

return $cachePool;
}

private function createRestartNotInCachePool(): CacheItemPoolInterface
{
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('isHit')->willReturn(false);
$cacheItem->method('get');
$cachePool->method('getItem')->willReturn($cacheItem);

return $cachePool;
}
}
Loading