diff --git a/Command/RpcServerCommand.php b/Command/RpcServerCommand.php index e52f9af8..54b7c0a6 100644 --- a/Command/RpcServerCommand.php +++ b/Command/RpcServerCommand.php @@ -2,12 +2,9 @@ namespace OldSound\RabbitMqBundle\Command; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -class RpcServerCommand extends BaseRabbitMqCommand +class RpcServerCommand extends BaseConsumerCommand { protected function configure(): void { @@ -16,35 +13,17 @@ protected function configure(): void $this ->setName('rabbitmq:rpc-server') ->setDescription('Start an RPC server') - ->addArgument('name', InputArgument::REQUIRED, 'Server Name') - ->addOption('messages', 'm', InputOption::VALUE_OPTIONAL, 'Messages to consume', '0') - ->addOption('debug', 'd', InputOption::VALUE_OPTIONAL, 'Debug mode', false) ; + + // Restore an RPC-specific description for the "name" argument, inherited + // from BaseConsumerCommand as "Consumer Name". + $this->getDefinition()->setArguments([ + new InputArgument('name', InputArgument::REQUIRED, 'Server Name'), + ]); } - /** - * Executes the current command. - * - * @param InputInterface $input An InputInterface instance - * @param OutputInterface $output An OutputInterface instance - * - * @return integer 0 if everything went fine, or an error code - * - * @throws \InvalidArgumentException When the number of messages to consume is less than 0 - */ - protected function execute(InputInterface $input, OutputInterface $output): int + protected function getConsumerService() { - define('AMQP_DEBUG', (bool) $input->getOption('debug')); - $amount = (int)$input->getOption('messages'); - - if (0 > $amount) { - throw new \InvalidArgumentException("The -m option should be null or greater than 0"); - } - - $this->getContainer() - ->get(sprintf('old_sound_rabbit_mq.%s_server', $input->getArgument('name'))) - ->start($amount); - - return 0; + return 'old_sound_rabbit_mq.%s_server'; } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 1f901f20..3abe34e3 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -420,6 +420,16 @@ protected function addRpcServers(ArrayNodeDefinition $node) ->children() ->scalarNode('connection')->defaultValue('default')->end() ->scalarNode('callback')->isRequired()->end() + ->scalarNode('idle_timeout')->end() + ->scalarNode('idle_timeout_exit_code')->end() + ->scalarNode('timeout_wait')->end() + ->arrayNode('graceful_max_execution') + ->canBeUnset() + ->children() + ->integerNode('timeout')->end() + ->integerNode('exit_code')->defaultValue(0)->end() + ->end() + ->end() ->arrayNode('qos_options') ->canBeUnset() ->children() diff --git a/DependencyInjection/OldSoundRabbitMqExtension.php b/DependencyInjection/OldSoundRabbitMqExtension.php index 735a5480..bb4ce7dc 100644 --- a/DependencyInjection/OldSoundRabbitMqExtension.php +++ b/DependencyInjection/OldSoundRabbitMqExtension.php @@ -666,6 +666,25 @@ protected function loadRpcServers() if (array_key_exists('serializer', $server)) { $definition->addMethodCall('setSerializer', [$server['serializer']]); } + if (isset($server['idle_timeout'])) { + $definition->addMethodCall('setIdleTimeout', [$server['idle_timeout']]); + } + if (isset($server['idle_timeout_exit_code'])) { + $definition->addMethodCall('setIdleTimeoutExitCode', [$server['idle_timeout_exit_code']]); + } + if (isset($server['timeout_wait'])) { + $definition->addMethodCall('setTimeoutWait', [$server['timeout_wait']]); + } + if (isset($server['graceful_max_execution'])) { + $definition->addMethodCall( + 'setGracefulMaxExecutionDateTimeFromSecondsInTheFuture', + [$server['graceful_max_execution']['timeout']] + ); + $definition->addMethodCall( + 'setGracefulMaxExecutionTimeoutExitCode', + [$server['graceful_max_execution']['exit_code']] + ); + } $this->container->setDefinition(sprintf('old_sound_rabbit_mq.%s_server', $key), $definition); } } diff --git a/Event/AMQPEvent.php b/Event/AMQPEvent.php index 8cd889dc..cd8ba6e8 100644 --- a/Event/AMQPEvent.php +++ b/Event/AMQPEvent.php @@ -2,7 +2,7 @@ namespace OldSound\RabbitMqBundle\Event; -use OldSound\RabbitMqBundle\RabbitMq\Consumer; +use OldSound\RabbitMqBundle\RabbitMq\BaseConsumer; use OldSound\RabbitMqBundle\RabbitMq\Producer; use PhpAmqpLib\Message\AMQPMessage; @@ -27,7 +27,7 @@ class AMQPEvent extends AbstractAMQPEvent protected $AMQPMessage; /** - * @var Consumer + * @var BaseConsumer */ protected $consumer; @@ -57,7 +57,7 @@ public function setAMQPMessage(AMQPMessage $AMQPMessage) } /** - * @return Consumer + * @return BaseConsumer */ public function getConsumer() { @@ -65,11 +65,11 @@ public function getConsumer() } /** - * @param Consumer $consumer + * @param BaseConsumer $consumer * * @return AMQPEvent */ - public function setConsumer(Consumer $consumer) + public function setConsumer(BaseConsumer $consumer) { $this->consumer = $consumer; diff --git a/Event/OnConsumeEvent.php b/Event/OnConsumeEvent.php index ef9bed2a..07a13fd4 100644 --- a/Event/OnConsumeEvent.php +++ b/Event/OnConsumeEvent.php @@ -2,7 +2,7 @@ namespace OldSound\RabbitMqBundle\Event; -use OldSound\RabbitMqBundle\RabbitMq\Consumer; +use OldSound\RabbitMqBundle\RabbitMq\BaseConsumer; /** * Class OnConsumeEvent @@ -16,9 +16,9 @@ class OnConsumeEvent extends AMQPEvent /** * OnConsumeEvent constructor. * - * @param Consumer $consumer + * @param BaseConsumer $consumer */ - public function __construct(Consumer $consumer) + public function __construct(BaseConsumer $consumer) { $this->setConsumer($consumer); } diff --git a/Event/OnIdleEvent.php b/Event/OnIdleEvent.php index 61a7d8d1..f7b23084 100644 --- a/Event/OnIdleEvent.php +++ b/Event/OnIdleEvent.php @@ -2,7 +2,7 @@ namespace OldSound\RabbitMqBundle\Event; -use OldSound\RabbitMqBundle\RabbitMq\Consumer; +use OldSound\RabbitMqBundle\RabbitMq\BaseConsumer; /** * Class OnIdleEvent @@ -21,9 +21,9 @@ class OnIdleEvent extends AMQPEvent /** * OnConsumeEvent constructor. * - * @param Consumer $consumer + * @param BaseConsumer $consumer */ - public function __construct(Consumer $consumer) + public function __construct(BaseConsumer $consumer) { $this->setConsumer($consumer); diff --git a/README.md b/README.md index b2b0642d..93506941 100644 --- a/README.md +++ b/README.md @@ -707,6 +707,12 @@ rpc_servers: exchange_options: {name: random_int, type: topic} queue_options: {name: random_int_queue, durable: false, auto_delete: true} serializer: json_encode + idle_timeout: 60 #default: ~ + idle_timeout_exit_code: 0 #default: ~ + timeout_wait: 10 #default: ~ + graceful_max_execution: + timeout: 1800 #default: ~ + exit_code: 10 #default: 0 ``` *For a full configuration reference please use the `php app/console config:dump-reference old_sound_rabbit_mq` command.* @@ -719,6 +725,18 @@ First we have to start the server from the command line: $ ./app/console_dev rabbitmq:rpc-server random_int ``` +The RPC server is a long-running consumer process, so `rabbitmq:rpc-server` accepts the same options as `rabbitmq:consumer`: + +- `--messages|-m` stops the server after the given number of requests; +- `--memory-limit|-l` sets a memory limit in MB — the server stops gracefully five MB before reaching it, so a supervisor can restart it (useful against slow memory leaks); +- `--without-signals|-w` disables the graceful handling of SIGTERM/SIGINT/SIGQUIT. + +The `idle_timeout`, `timeout_wait` and `graceful_max_execution` options shown above behave exactly as they do for consumers. + +```bash +$ ./app/console_dev rabbitmq:rpc-server random_int -l 256 +``` + And then add the following code to our controller: ```php diff --git a/RabbitMq/BaseConsumer.php b/RabbitMq/BaseConsumer.php index c3935ee8..b01d8817 100644 --- a/RabbitMq/BaseConsumer.php +++ b/RabbitMq/BaseConsumer.php @@ -2,6 +2,11 @@ namespace OldSound\RabbitMqBundle\RabbitMq; +use OldSound\RabbitMqBundle\Event\OnConsumeEvent; +use OldSound\RabbitMqBundle\Event\OnIdleEvent; +use OldSound\RabbitMqBundle\MemoryChecker\MemoryConsumptionChecker; +use OldSound\RabbitMqBundle\MemoryChecker\NativeMemoryUsageProvider; +use PhpAmqpLib\Exception\AMQPTimeoutException; use PhpAmqpLib\Message\AMQPMessage; abstract class BaseConsumer extends BaseAmqp implements DequeuerInterface @@ -24,6 +29,32 @@ abstract class BaseConsumer extends BaseAmqp implements DequeuerInterface /** @var int */ protected $idleTimeoutExitCode; + /** + * @var int|null $memoryLimit + */ + protected $memoryLimit = null; + + /** + * @var \DateTime|null DateTime after which the consumer will gracefully exit. "Gracefully" means, that + * any currently running consumption will not be interrupted. + */ + protected $gracefulMaxExecutionDateTime; + + /** + * @var int Exit code used, when consumer is closed by the Graceful Max Execution Timeout feature. + */ + protected $gracefulMaxExecutionTimeoutExitCode = 0; + + /** + * @var int|null + */ + protected $timeoutWait; + + /** + * @var \DateTime|null + */ + protected $lastActivityDateTime; + public function setCallback($callback) { $this->callback = $callback; @@ -37,6 +68,26 @@ public function getCallback() return $this->callback; } + /** + * Set the memory limit + * + * @param int $memoryLimit + */ + public function setMemoryLimit($memoryLimit) + { + $this->memoryLimit = $memoryLimit; + } + + /** + * Get the memory limit + * + * @return int|null + */ + public function getMemoryLimit() + { + return $this->memoryLimit; + } + /** * @param int $msgAmount * @throws \ErrorException @@ -52,6 +103,70 @@ public function start($msgAmount = 0) } } + /** + * Consume the message + * + * @param int $msgAmount + * + * @return int + * + * @throws AMQPTimeoutException + */ + public function consume($msgAmount) + { + $this->target = $msgAmount; + + $this->setupConsumer(); + + $this->setLastActivityDateTime(new \DateTime()); + while ($this->getChannel()->is_consuming()) { + $this->dispatchEvent(OnConsumeEvent::NAME, new OnConsumeEvent($this)); + $this->maybeStopConsumer(); + + /* + * Be careful not to trigger ::wait() with 0 or less seconds, when + * graceful max execution timeout is being used. + */ + + $waitTimeout = $this->chooseWaitTimeout(); + if ($this->gracefulMaxExecutionDateTime + && $waitTimeout < 1 + ) { + return $this->gracefulMaxExecutionTimeoutExitCode; + } + + if (!$this->forceStop) { + try { + $this->getChannel()->wait(null, false, $waitTimeout); + $this->setLastActivityDateTime(new \DateTime()); + } catch (AMQPTimeoutException $e) { + $now = time(); + + if ($this->gracefulMaxExecutionDateTime + && $this->gracefulMaxExecutionDateTime <= new \DateTime("@$now") + ) { + return $this->gracefulMaxExecutionTimeoutExitCode; + } elseif ($this->getIdleTimeout() + && ($this->getLastActivityDateTime()->getTimestamp() + $this->getIdleTimeout() <= $now) + ) { + $idleEvent = new OnIdleEvent($this); + $this->dispatchEvent(OnIdleEvent::NAME, $idleEvent); + + if ($idleEvent->isForceStop()) { + if (null !== $this->getIdleTimeoutExitCode()) { + return $this->getIdleTimeoutExitCode(); + } else { + throw $e; + } + } + } + } + } + } + + return 0; + } + /** * Tell the server you are going to stop consuming. * @@ -88,9 +203,30 @@ protected function maybeStopConsumer() if ($this->forceStop || ($this->consumed == $this->target && $this->target > 0)) { $this->stopConsuming(); + + return; + } + + if (!is_null($this->getMemoryLimit()) && $this->isRamAlmostOverloaded()) { + // Also raise the force-stop flag so the consume() loop exits instead + // of entering a wait() that can block forever on a cancelled consumer. + $this->forceStopConsumer(); + $this->stopConsuming(); } } + /** + * Checks if memory in use is greater or equal than memory allowed for this process + * + * @return boolean + */ + protected function isRamAlmostOverloaded() + { + $memoryManager = new MemoryConsumptionChecker(new NativeMemoryUsageProvider()); + + return $memoryManager->isRamAlmostOverloaded($this->getMemoryLimit().'M', '5M'); + } + public function setConsumerTag($tag) { $this->consumerTag = $tag; @@ -149,6 +285,56 @@ public function getIdleTimeoutExitCode() return $this->idleTimeoutExitCode; } + /** + * @param \DateTime|null $dateTime + */ + public function setGracefulMaxExecutionDateTime(?\DateTime $dateTime = null) + { + $this->gracefulMaxExecutionDateTime = $dateTime; + } + + /** + * @param int $secondsInTheFuture + */ + public function setGracefulMaxExecutionDateTimeFromSecondsInTheFuture($secondsInTheFuture) + { + $this->setGracefulMaxExecutionDateTime(new \DateTime("+{$secondsInTheFuture} seconds")); + } + + /** + * @param int $exitCode + */ + public function setGracefulMaxExecutionTimeoutExitCode($exitCode) + { + $this->gracefulMaxExecutionTimeoutExitCode = $exitCode; + } + + public function setTimeoutWait(int $timeoutWait): void + { + $this->timeoutWait = $timeoutWait; + } + + /** + * @return \DateTime|null + */ + public function getGracefulMaxExecutionDateTime() + { + return $this->gracefulMaxExecutionDateTime; + } + + /** + * @return int + */ + public function getGracefulMaxExecutionTimeoutExitCode() + { + return $this->gracefulMaxExecutionTimeoutExitCode; + } + + public function getTimeoutWait(): ?int + { + return $this->timeoutWait; + } + /** * Resets the consumed property. * Use when you want to call start() or consume() multiple times. @@ -157,4 +343,51 @@ public function resetConsumed() { $this->consumed = 0; } + + /** + * Choose the timeout wait (in seconds) to use for the $this->getChannel()->wait() method. + */ + private function chooseWaitTimeout(): int + { + if ($this->gracefulMaxExecutionDateTime) { + $allowedExecutionDateInterval = $this->gracefulMaxExecutionDateTime->diff(new \DateTime()); + $allowedExecutionSeconds = $allowedExecutionDateInterval->days * 86400 + + $allowedExecutionDateInterval->h * 3600 + + $allowedExecutionDateInterval->i * 60 + + $allowedExecutionDateInterval->s; + + if (!$allowedExecutionDateInterval->invert) { + $allowedExecutionSeconds *= -1; + } + + /* + * Respect the idle timeout if it's set and if it's less than + * the remaining allowed execution. + */ + if ($this->getIdleTimeout() + && $this->getIdleTimeout() < $allowedExecutionSeconds + ) { + $waitTimeout = $this->getIdleTimeout(); + } else { + $waitTimeout = $allowedExecutionSeconds; + } + } else { + $waitTimeout = $this->getIdleTimeout(); + } + + if (!is_null($this->getTimeoutWait()) && $this->getTimeoutWait() > 0) { + $waitTimeout = min($waitTimeout, $this->getTimeoutWait()); + } + return $waitTimeout; + } + + public function setLastActivityDateTime(\DateTime $dateTime) + { + $this->lastActivityDateTime = $dateTime; + } + + protected function getLastActivityDateTime(): ?\DateTime + { + return $this->lastActivityDateTime; + } } diff --git a/RabbitMq/Consumer.php b/RabbitMq/Consumer.php index 9df43f07..7fde7603 100644 --- a/RabbitMq/Consumer.php +++ b/RabbitMq/Consumer.php @@ -4,125 +4,10 @@ use OldSound\RabbitMqBundle\Event\AfterProcessingMessageEvent; use OldSound\RabbitMqBundle\Event\BeforeProcessingMessageEvent; -use OldSound\RabbitMqBundle\Event\OnConsumeEvent; -use OldSound\RabbitMqBundle\Event\OnIdleEvent; -use OldSound\RabbitMqBundle\MemoryChecker\MemoryConsumptionChecker; -use OldSound\RabbitMqBundle\MemoryChecker\NativeMemoryUsageProvider; -use PhpAmqpLib\Exception\AMQPTimeoutException; use PhpAmqpLib\Message\AMQPMessage; class Consumer extends BaseConsumer { - /** - * @var int|null $memoryLimit - */ - protected $memoryLimit = null; - - /** - * @var \DateTime|null DateTime after which the consumer will gracefully exit. "Gracefully" means, that - * any currently running consumption will not be interrupted. - */ - protected $gracefulMaxExecutionDateTime; - - /** - * @var int Exit code used, when consumer is closed by the Graceful Max Execution Timeout feature. - */ - protected $gracefulMaxExecutionTimeoutExitCode = 0; - - /** - * @var int|null - */ - protected $timeoutWait; - - /** - * @var \DateTime|null - */ - protected $lastActivityDateTime; - - /** - * Set the memory limit - * - * @param int $memoryLimit - */ - public function setMemoryLimit($memoryLimit) - { - $this->memoryLimit = $memoryLimit; - } - - /** - * Get the memory limit - * - * @return int|null - */ - public function getMemoryLimit() - { - return $this->memoryLimit; - } - - /** - * Consume the message - * - * @param int $msgAmount - * - * @return int - * - * @throws AMQPTimeoutException - */ - public function consume($msgAmount) - { - $this->target = $msgAmount; - - $this->setupConsumer(); - - $this->setLastActivityDateTime(new \DateTime()); - while ($this->getChannel()->is_consuming()) { - $this->dispatchEvent(OnConsumeEvent::NAME, new OnConsumeEvent($this)); - $this->maybeStopConsumer(); - - /* - * Be careful not to trigger ::wait() with 0 or less seconds, when - * graceful max execution timeout is being used. - */ - - $waitTimeout = $this->chooseWaitTimeout(); - if ($this->gracefulMaxExecutionDateTime - && $waitTimeout < 1 - ) { - return $this->gracefulMaxExecutionTimeoutExitCode; - } - - if (!$this->forceStop) { - try { - $this->getChannel()->wait(null, false, $waitTimeout); - $this->setLastActivityDateTime(new \DateTime()); - } catch (AMQPTimeoutException $e) { - $now = time(); - - if ($this->gracefulMaxExecutionDateTime - && $this->gracefulMaxExecutionDateTime <= new \DateTime("@$now") - ) { - return $this->gracefulMaxExecutionTimeoutExitCode; - } elseif ($this->getIdleTimeout() - && ($this->getLastActivityDateTime()->getTimestamp() + $this->getIdleTimeout() <= $now) - ) { - $idleEvent = new OnIdleEvent($this); - $this->dispatchEvent(OnIdleEvent::NAME, $idleEvent); - - if ($idleEvent->isForceStop()) { - if (null !== $this->getIdleTimeoutExitCode()) { - return $this->getIdleTimeoutExitCode(); - } else { - throw $e; - } - } - } - } - } - } - - return 0; - } - /** * Purge the queue */ @@ -213,118 +98,5 @@ protected function handleProcessMessage(AMQPMessage $msg, $processFlag) $this->consumed++; $this->maybeStopConsumer(); - - if (!is_null($this->getMemoryLimit()) && $this->isRamAlmostOverloaded()) { - $this->stopConsuming(); - } - } - - /** - * Checks if memory in use is greater or equal than memory allowed for this process - * - * @return boolean - */ - protected function isRamAlmostOverloaded() - { - $memoryManager = new MemoryConsumptionChecker(new NativeMemoryUsageProvider()); - - return $memoryManager->isRamAlmostOverloaded($this->getMemoryLimit().'M', '5M'); - } - - /** - * @param \DateTime|null $dateTime - */ - public function setGracefulMaxExecutionDateTime(?\DateTime $dateTime = null) - { - $this->gracefulMaxExecutionDateTime = $dateTime; - } - - /** - * @param int $secondsInTheFuture - */ - public function setGracefulMaxExecutionDateTimeFromSecondsInTheFuture($secondsInTheFuture) - { - $this->setGracefulMaxExecutionDateTime(new \DateTime("+{$secondsInTheFuture} seconds")); - } - - /** - * @param int $exitCode - */ - public function setGracefulMaxExecutionTimeoutExitCode($exitCode) - { - $this->gracefulMaxExecutionTimeoutExitCode = $exitCode; - } - - public function setTimeoutWait(int $timeoutWait): void - { - $this->timeoutWait = $timeoutWait; - } - - /** - * @return \DateTime|null - */ - public function getGracefulMaxExecutionDateTime() - { - return $this->gracefulMaxExecutionDateTime; - } - - /** - * @return int - */ - public function getGracefulMaxExecutionTimeoutExitCode() - { - return $this->gracefulMaxExecutionTimeoutExitCode; - } - - public function getTimeoutWait(): ?int - { - return $this->timeoutWait; - } - - /** - * Choose the timeout wait (in seconds) to use for the $this->getChannel()->wait() method. - */ - private function chooseWaitTimeout(): int - { - if ($this->gracefulMaxExecutionDateTime) { - $allowedExecutionDateInterval = $this->gracefulMaxExecutionDateTime->diff(new \DateTime()); - $allowedExecutionSeconds = $allowedExecutionDateInterval->days * 86400 - + $allowedExecutionDateInterval->h * 3600 - + $allowedExecutionDateInterval->i * 60 - + $allowedExecutionDateInterval->s; - - if (!$allowedExecutionDateInterval->invert) { - $allowedExecutionSeconds *= -1; - } - - /* - * Respect the idle timeout if it's set and if it's less than - * the remaining allowed execution. - */ - if ($this->getIdleTimeout() - && $this->getIdleTimeout() < $allowedExecutionSeconds - ) { - $waitTimeout = $this->getIdleTimeout(); - } else { - $waitTimeout = $allowedExecutionSeconds; - } - } else { - $waitTimeout = $this->getIdleTimeout(); - } - - if (!is_null($this->getTimeoutWait()) && $this->getTimeoutWait() > 0) { - $waitTimeout = min($waitTimeout, $this->getTimeoutWait()); - } - return $waitTimeout; - } - - public function setLastActivityDateTime(\DateTime $dateTime) - { - $this->lastActivityDateTime = $dateTime; - } - - protected function getLastActivityDateTime(): ?\DateTime - { - return $this->lastActivityDateTime; } } diff --git a/Tests/Command/RpcServerCommandTest.php b/Tests/Command/RpcServerCommandTest.php new file mode 100644 index 00000000..13345096 --- /dev/null +++ b/Tests/Command/RpcServerCommandTest.php @@ -0,0 +1,54 @@ +application = $this->getMockBuilder(Application::class)->disableOriginalConstructor()->getMock(); + $this->definition = $this->getMockBuilder(InputDefinition::class)->disableOriginalConstructor()->getMock(); + $this->helperSet = $this->getMockBuilder(HelperSet::class)->getMock(); + + $this->application->method('getDefinition')->willReturn($this->definition); + $this->definition->method('getArguments')->willReturn([]); + $this->definition->method('getOptions')->willReturn([ + new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Increase verbosity of messages.'), + new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', 'dev'), + new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switches off debug mode.'), + ]); + + $this->application->expects($this->once())->method('getHelperSet')->willReturn($this->helperSet); + + $this->command = new RpcServerCommand(); + $this->command->setApplication($this->application); +}); + +test('rpc server command has the correct name', function () { + expect($this->command->getName())->toBe('rabbitmq:rpc-server'); +}); + +test('rpc server command has the correct input definition', function () { + $definition = $this->command->getDefinition(); + + expect($definition->hasArgument('name'))->toBeTrue(); + expect($definition->getArgument('name')->isRequired())->toBeTrue(); + expect($definition->getArgument('name')->getDescription())->toBe('Server Name'); + + expect($definition->hasOption('messages'))->toBeTrue(); + expect($definition->getOption('messages')->isValueOptional())->toBeTrue(); + + expect($definition->hasOption('memory-limit'))->toBeTrue(); + expect($definition->getOption('memory-limit')->isValueOptional())->toBeTrue(); + expect($definition->getOption('memory-limit')->getShortcut())->toBe('l'); + + expect($definition->hasOption('route'))->toBeTrue(); + expect($definition->getOption('route')->isValueOptional())->toBeTrue(); + + expect($definition->hasOption('without-signals'))->toBeTrue(); + expect($definition->getOption('without-signals')->acceptValue())->toBeFalse(); + + expect($definition->hasOption('debug'))->toBeTrue(); + expect($definition->getOption('debug')->acceptValue())->toBeFalse(); +}); diff --git a/Tests/DependencyInjection/Fixtures/test.yml b/Tests/DependencyInjection/Fixtures/test.yml index 4ec9d171..fb778104 100644 --- a/Tests/DependencyInjection/Fixtures/test.yml +++ b/Tests/DependencyInjection/Fixtures/test.yml @@ -247,6 +247,15 @@ old_sound_rabbit_mq: exchange_options: name: exchange type: topic + + server_with_timeouts: + callback: server_with_timeouts.callback + idle_timeout: 60 + idle_timeout_exit_code: 2 + timeout_wait: 10 + graceful_max_execution: + timeout: 1800 + exit_code: 10 services: foo.callback: default.callback: @@ -263,3 +272,4 @@ services: default_server.callback: server_with_queue_options.callback: server_with_exchange_options.callback: + server_with_timeouts.callback: diff --git a/Tests/DependencyInjection/OldSoundRabbitMqExtensionTest.php b/Tests/DependencyInjection/OldSoundRabbitMqExtensionTest.php index 80991fe3..688bf02a 100644 --- a/Tests/DependencyInjection/OldSoundRabbitMqExtensionTest.php +++ b/Tests/DependencyInjection/OldSoundRabbitMqExtensionTest.php @@ -483,6 +483,26 @@ function assertBindingMethodCallsEqual(Definition $definition, array $binding): expect($definition->getClass())->toBe('%old_sound_rabbit_mq.rpc_server.class%'); }); +test('rpc server with timeouts definition', function () { + $container = buildContainer('test.yml'); + $definition = $container->getDefinition('old_sound_rabbit_mq.server_with_timeouts_server'); + + expect($container->has('old_sound_rabbit_mq.server_with_timeouts_server'))->toBeTrue(); + expect((string) $definition->getArgument(0))->toBe('old_sound_rabbit_mq.connection.default'); + expect((string) $definition->getArgument(1))->toBe('old_sound_rabbit_mq.channel.server_with_timeouts'); + expect($definition->getMethodCalls())->toEqual([ + ['initServer', ['server_with_timeouts']], + ['setCallback', [[new Reference('server_with_timeouts.callback'), 'execute']]], + ['setSerializer', ['serialize']], + ['setIdleTimeout', [60]], + ['setIdleTimeoutExitCode', [2]], + ['setTimeoutWait', [10]], + ['setGracefulMaxExecutionDateTimeFromSecondsInTheFuture', [1800]], + ['setGracefulMaxExecutionTimeoutExitCode', [10]], + ]); + expect($definition->getClass())->toBe('%old_sound_rabbit_mq.rpc_server.class%'); +}); + test('rpc server with exchange options definition', function () { $container = buildContainer('test.yml'); $definition = $container->getDefinition('old_sound_rabbit_mq.server_with_exchange_options_server'); diff --git a/Tests/RabbitMq/ConsumerTest.php b/Tests/RabbitMq/ConsumerTest.php index 6835c85d..5e7eb5fe 100644 --- a/Tests/RabbitMq/ConsumerTest.php +++ b/Tests/RabbitMq/ConsumerTest.php @@ -109,6 +109,54 @@ 'with no messages' => [['messages' => []]], ]); +test('process message stops consuming when memory limit is almost reached', function (string $consumerClass) { + $amqpConnection = $this->getMockBuilder(AMQPStreamConnection::class)->disableOriginalConstructor()->getMock(); + $amqpChannel = $this->getMockBuilder(AMQPChannel::class)->disableOriginalConstructor()->getMock(); + + $consumer = $this->getMockBuilder($consumerClass) + ->setConstructorArgs([$amqpConnection, $amqpChannel]) + ->onlyMethods(['isRamAlmostOverloaded', 'stopConsuming']) + ->getMock(); + + $consumer->setCallback(static fn () => true); + $consumer->setMemoryLimit(128); + + $consumer->expects($this->once())->method('isRamAlmostOverloaded')->willReturn(true); + $consumer->expects($this->once())->method('stopConsuming'); + + $amqpMessage = new AMQPMessage('foo body'); + $amqpMessage->setChannel($amqpChannel); + $amqpMessage->setDeliveryTag(0); + + $consumer->processMessage($amqpMessage); +})->with('consumer_classes'); + +test('consume exits without waiting when memory limit is already exceeded', function (string $consumerClass) { + $amqpConnection = $this->getMockBuilder(AMQPStreamConnection::class)->disableOriginalConstructor()->getMock(); + $amqpChannel = $this->getMockBuilder(AMQPChannel::class)->disableOriginalConstructor()->getMock(); + + $amqpChannel->method('getChannelId')->willReturn(true); + $amqpChannel->expects($this->once())->method('basic_consume')->withAnyParameters()->willReturn(true); + $amqpChannel->expects(self::exactly(2)) + ->method('is_consuming') + ->willReturnOnConsecutiveCalls(true, false); + $amqpChannel->expects($this->once())->method('basic_cancel'); + // The consumer is already cancelled, so wait() would block indefinitely. + $amqpChannel->expects($this->never())->method('wait'); + + $consumer = $this->getMockBuilder($consumerClass) + ->setConstructorArgs([$amqpConnection, $amqpChannel]) + ->onlyMethods(['isRamAlmostOverloaded']) + ->getMock(); + $consumer->method('isRamAlmostOverloaded')->willReturn(true); + + $consumer->disableAutoSetupFabric(); + $consumer->setChannel($amqpChannel); + $consumer->setMemoryLimit(1); + + expect($consumer->consume(1))->toBe(0); +})->with('consumer_classes'); + test('idle timeout returns configured exit code', function (string $consumerClass) { $amqpConnection = $this->getMockBuilder(AMQPStreamConnection::class)->disableOriginalConstructor()->getMock(); $amqpChannel = $this->getMockBuilder(AMQPChannel::class)->disableOriginalConstructor()->getMock(); diff --git a/Tests/RabbitMq/RpcServerTest.php b/Tests/RabbitMq/RpcServerTest.php index 1f68e42e..a0e424b4 100644 --- a/Tests/RabbitMq/RpcServerTest.php +++ b/Tests/RabbitMq/RpcServerTest.php @@ -31,3 +31,54 @@ $server->processMessage($message); }); + +test('process message stops consuming when memory limit is almost reached', function () { + $server = $this->getMockBuilder(RpcServer::class) + ->onlyMethods(['sendReply', 'isRamAlmostOverloaded', 'stopConsuming']) + ->disableOriginalConstructor() + ->getMock(); + + $message = $this->getMockBuilder(AMQPMessage::class) + ->onlyMethods(['get']) + ->getMock(); + + $channel = $this->getMockBuilder('\PhpAmqpLib\Channel\AMQPChannel') + ->disableOriginalConstructor() + ->getMock(); + + $message->setChannel($channel); + $message->setDeliveryTag(0); + + $server->setCallback(static fn () => 'message'); + $server->setMemoryLimit(1); + + $server->expects($this->once())->method('isRamAlmostOverloaded')->willReturn(true); + $server->expects($this->once())->method('stopConsuming'); + + $server->processMessage($message); +}); + +test('process message does not check memory when no limit is set', function () { + $server = $this->getMockBuilder(RpcServer::class) + ->onlyMethods(['sendReply', 'isRamAlmostOverloaded', 'stopConsuming']) + ->disableOriginalConstructor() + ->getMock(); + + $message = $this->getMockBuilder(AMQPMessage::class) + ->onlyMethods(['get']) + ->getMock(); + + $channel = $this->getMockBuilder('\PhpAmqpLib\Channel\AMQPChannel') + ->disableOriginalConstructor() + ->getMock(); + + $message->setChannel($channel); + $message->setDeliveryTag(0); + + $server->setCallback(static fn () => 'message'); + + $server->expects($this->never())->method('isRamAlmostOverloaded'); + $server->expects($this->never())->method('stopConsuming'); + + $server->processMessage($message); +});