| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- <?php declare(strict_types=1);
- namespace mikemadisonweb\rabbitmq\components;
- use BadFunctionCallException;
- use ErrorException;
- use mikemadisonweb\rabbitmq\events\RabbitMQConsumerEvent;
- use mikemadisonweb\rabbitmq\exceptions\RuntimeException;
- use PhpAmqpLib\Exception\AMQPTimeoutException;
- use PhpAmqpLib\Message\AMQPMessage;
- use Throwable;
- use yii\console\Controller;
- /**
- * Service that receives AMQP Messages
- *
- * @package mikemadisonweb\rabbitmq\components
- */
- class Consumer extends BaseRabbitMQ
- {
- protected $deserializer;
- protected $qos;
- protected $idleTimeout;
- protected $idleTimeoutExitCode;
- protected $queues = [];
- protected $memoryLimit = 0;
- protected $proceedOnException;
- protected $name = 'unnamed';
- private $id;
- private $target;
- private $consumed = 0;
- private $forceStop = false;
- /**
- * Set the memory limit
- *
- * @param int $memoryLimit
- */
- public function setMemoryLimit($memoryLimit)
- {
- $this->memoryLimit = $memoryLimit;
- }
- /**
- * Get the memory limit
- *
- * @return int
- */
- public function getMemoryLimit(): int
- {
- return $this->memoryLimit;
- }
- /**
- * @param array $queues
- */
- public function setQueues(array $queues)
- {
- $this->queues = $queues;
- }
- /**
- * @return array
- */
- public function getQueues(): array
- {
- return $this->queues;
- }
- /**
- * @param $idleTimeout
- */
- public function setIdleTimeout($idleTimeout)
- {
- $this->idleTimeout = $idleTimeout;
- }
- public function getIdleTimeout()
- {
- return $this->idleTimeout;
- }
- /**
- * Set exit code to be returned when there is a timeout exception
- *
- * @param int|null $idleTimeoutExitCode
- */
- public function setIdleTimeoutExitCode($idleTimeoutExitCode)
- {
- $this->idleTimeoutExitCode = $idleTimeoutExitCode;
- }
- /**
- * Get exit code to be returned when there is a timeout exception
- *
- * @return int|null
- */
- public function getIdleTimeoutExitCode()
- {
- return $this->idleTimeoutExitCode;
- }
- /**
- * @return mixed
- */
- public function getDeserializer(): callable
- {
- return $this->deserializer;
- }
- /**
- * @param mixed $deserializer
- */
- public function setDeserializer(callable $deserializer)
- {
- $this->deserializer = $deserializer;
- }
- /**
- * @return mixed
- */
- public function getQos(): array
- {
- return $this->qos;
- }
- /**
- * @param mixed $qos
- */
- public function setQos(array $qos)
- {
- $this->qos = $qos;
- }
- /**
- * @param string $name
- */
- public function setName(string $name)
- {
- $this->name = $name;
- }
- /**
- * @return string
- */
- public function getName(): string
- {
- return $this->name;
- }
- /**
- * Resets the consumed property.
- * Use when you want to call start() or consume() multiple times.
- */
- public function getConsumed(): int
- {
- return $this->consumed;
- }
- /**
- * Resets the consumed property.
- * Use when you want to call start() or consume() multiple times.
- */
- public function resetConsumed()
- {
- $this->consumed = 0;
- }
- /**
- * @return mixed
- */
- public function getProceedOnException(): bool
- {
- return $this->proceedOnException;
- }
- /**
- * @param mixed $proceedOnException
- */
- public function setProceedOnException(bool $proceedOnException)
- {
- $this->proceedOnException = $proceedOnException;
- }
- /**
- * Consume designated number of messages (0 means infinite)
- *
- * @param int $msgAmount
- *
- * @return int
- * @throws BadFunctionCallException
- * @throws RuntimeException
- * @throws AMQPTimeoutException
- * @throws ErrorException
- */
- public function consume($msgAmount = 0): int
- {
- $this->target = $msgAmount;
- $this->setup();
- // At the end of the callback execution
- while (count($this->getChannel()->callbacks))
- {
- if ($this->maybeStopConsumer())
- {
- break;
- }
- try
- {
- $this->getChannel()->wait(null, false, $this->getIdleTimeout());
- }
- catch (AMQPTimeoutException $e)
- {
- if (null !== $this->getIdleTimeoutExitCode())
- {
- return $this->getIdleTimeoutExitCode();
- }
- throw $e;
- }
- if (!AMQP_WITHOUT_SIGNALS && extension_loaded('pcntl'))
- {
- pcntl_signal_dispatch();
- }
- }
- return Controller::EXIT_CODE_NORMAL;
- }
- /**
- * Stop consuming messages
- */
- public function stopConsuming()
- {
- foreach ($this->queues as $name => $options)
- {
- $this->getChannel()->basic_cancel($this->getConsumerTag($name), false, true);
- }
- }
- /**
- * Force stop the consumer
- */
- public function stopDaemon()
- {
- $this->forceStop = true;
- $this->stopConsuming();
- $this->logger->printInfo("\nConsumer stopped by user.\n");
- }
- /**
- * Force restart the consumer
- */
- public function restartDaemon()
- {
- $this->stopConsuming();
- $this->renew();
- $this->setup();
- $this->logger->printInfo("\nConsumer has been restarted.\n");
- }
- /**
- * Sets the qos settings for the current channel
- * This method needs a connection to broker
- */
- protected function setQosOptions()
- {
- if (empty($this->qos))
- {
- return;
- }
- $prefetchSize = $this->qos['prefetch_size'] ?? null;
- $prefetchCount = $this->qos['prefetch_count'] ?? null;
- $global = $this->qos['global'] ?? null;
- $this->getChannel()->basic_qos($prefetchSize, $prefetchCount, $global);
- }
- /**
- * Start consuming messages
- *
- * @throws RuntimeException
- */
- protected function startConsuming()
- {
- $this->id = $this->generateUniqueId();
- foreach ($this->queues as $queue => $callback)
- {
- $that = $this;
- $this->getChannel()->basic_consume(
- $queue,
- $this->getConsumerTag($queue),
- null,
- null,
- null,
- null,
- function (AMQPMessage $msg) use ($that, $queue, $callback)
- {
- // Execute user-defined callback
- $that->onReceive($msg, $queue, $callback);
- }
- );
- }
- }
- /**
- * Decide whether it's time to stop consuming
- *
- * @throws BadFunctionCallException
- */
- protected function maybeStopConsumer(): bool
- {
- if (extension_loaded('pcntl') && (defined('AMQP_WITHOUT_SIGNALS') ? !AMQP_WITHOUT_SIGNALS : true))
- {
- if (!function_exists('pcntl_signal_dispatch'))
- {
- throw new BadFunctionCallException(
- "Function 'pcntl_signal_dispatch' is referenced in the php.ini 'disable_functions' and can't be called."
- );
- }
- pcntl_signal_dispatch();
- }
- if ($this->forceStop || ($this->consumed === $this->target && $this->target > 0))
- {
- $this->stopConsuming();
- return true;
- }
- if (0 !== $this->getMemoryLimit() && $this->isRamAlmostOverloaded())
- {
- $this->stopConsuming();
- return true;
- }
- return false;
- }
- /**
- * Callback that will be fired upon receiving new message
- *
- * @param AMQPMessage $msg
- * @param $queueName
- * @param $callback
- *
- * @return bool
- * @throws Throwable
- */
- protected function onReceive(AMQPMessage $msg, string $queueName, callable $callback): bool
- {
- $timeStart = microtime(true);
- \Yii::$app->rabbitmq->trigger(
- RabbitMQConsumerEvent::BEFORE_CONSUME,
- new RabbitMQConsumerEvent(
- [
- 'message' => $msg,
- 'consumer' => $this,
- ]
- )
- );
- try
- {
- // deserialize message back to initial data type
- if ($msg->has('application_headers') &&
- isset($msg->get('application_headers')->getNativeData()['rabbitmq.serialized']))
- {
- $msg->setBody(call_user_func($this->deserializer, $msg->getBody()));
- }
- // process message and return the result code back to broker
- $processFlag = $callback($msg);
- $this->sendResult($msg, $processFlag);
- \Yii::$app->rabbitmq->trigger(
- RabbitMQConsumerEvent::AFTER_CONSUME,
- new RabbitMQConsumerEvent(
- [
- 'message' => $msg,
- 'consumer' => $this,
- ]
- )
- );
- $this->logger->printResult($queueName, $processFlag, $timeStart);
- $this->logger->log(
- 'Queue message processed.',
- $msg,
- [
- 'queue' => $queueName,
- 'processFlag' => $processFlag,
- 'timeStart' => $timeStart,
- 'memory' => true,
- ]
- );
- }
- catch (Throwable $e)
- {
- $this->logger->logError($e, $msg);
- if (!$this->proceedOnException)
- {
- throw $e;
- }
- }
- $this->consumed++;
- return true;
- }
- /**
- * Mark message status based on return code from callback
- *
- * @param AMQPMessage $msg
- * @param $processFlag
- */
- protected function sendResult(AMQPMessage $msg, $processFlag)
- {
- // true in testing environment
- if (!isset($msg->delivery_info['channel']))
- {
- return;
- }
- // respond to the broker with appropriate reply code
- if ($processFlag === ConsumerInterface::MSG_REQUEUE || false === $processFlag)
- {
- // Reject and requeue message to RabbitMQ
- $msg->delivery_info['channel']->basic_reject($msg->delivery_info['delivery_tag'], true);
- }
- elseif ($processFlag === ConsumerInterface::MSG_REJECT)
- {
- // Reject and drop
- $msg->delivery_info['channel']->basic_reject($msg->delivery_info['delivery_tag'], false);
- }
- else
- {
- // Remove message from queue only if callback return not false
- $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
- }
- }
- /**
- * Checks if memory in use is greater or equal than memory allowed for this process
- *
- * @return boolean
- */
- protected function isRamAlmostOverloaded(): bool
- {
- return memory_get_usage(true) >= ($this->getMemoryLimit() * 1024 * 1024);
- }
- /**
- * @param string $queueName
- *
- * @return string
- */
- protected function getConsumerTag(string $queueName): string
- {
- return sprintf('%s-%s-%s', $queueName, $this->name, $this->id);
- }
- /**
- * @return string
- */
- protected function generateUniqueId(): string
- {
- return uniqid('rabbitmq_', true);
- }
- protected function setup()
- {
- $this->resetConsumed();
- if ($this->autoDeclare)
- {
- $this->routing->declareAll();
- }
- $this->setQosOptions();
- $this->startConsuming();
- }
- }
|