Consumer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <?php declare(strict_types=1);
  2. namespace mikemadisonweb\rabbitmq\components;
  3. use BadFunctionCallException;
  4. use ErrorException;
  5. use mikemadisonweb\rabbitmq\events\RabbitMQConsumerEvent;
  6. use mikemadisonweb\rabbitmq\exceptions\RuntimeException;
  7. use PhpAmqpLib\Exception\AMQPTimeoutException;
  8. use PhpAmqpLib\Message\AMQPMessage;
  9. use Throwable;
  10. use yii\console\Controller;
  11. /**
  12. * Service that receives AMQP Messages
  13. *
  14. * @package mikemadisonweb\rabbitmq\components
  15. */
  16. class Consumer extends BaseRabbitMQ
  17. {
  18. protected $deserializer;
  19. protected $qos;
  20. protected $idleTimeout;
  21. protected $idleTimeoutExitCode;
  22. protected $queues = [];
  23. protected $memoryLimit = 0;
  24. protected $proceedOnException;
  25. protected $name = 'unnamed';
  26. private $id;
  27. private $target;
  28. private $consumed = 0;
  29. private $forceStop = false;
  30. /**
  31. * Set the memory limit
  32. *
  33. * @param int $memoryLimit
  34. */
  35. public function setMemoryLimit($memoryLimit)
  36. {
  37. $this->memoryLimit = $memoryLimit;
  38. }
  39. /**
  40. * Get the memory limit
  41. *
  42. * @return int
  43. */
  44. public function getMemoryLimit(): int
  45. {
  46. return $this->memoryLimit;
  47. }
  48. /**
  49. * @param array $queues
  50. */
  51. public function setQueues(array $queues)
  52. {
  53. $this->queues = $queues;
  54. }
  55. /**
  56. * @return array
  57. */
  58. public function getQueues(): array
  59. {
  60. return $this->queues;
  61. }
  62. /**
  63. * @param $idleTimeout
  64. */
  65. public function setIdleTimeout($idleTimeout)
  66. {
  67. $this->idleTimeout = $idleTimeout;
  68. }
  69. public function getIdleTimeout()
  70. {
  71. return $this->idleTimeout;
  72. }
  73. /**
  74. * Set exit code to be returned when there is a timeout exception
  75. *
  76. * @param int|null $idleTimeoutExitCode
  77. */
  78. public function setIdleTimeoutExitCode($idleTimeoutExitCode)
  79. {
  80. $this->idleTimeoutExitCode = $idleTimeoutExitCode;
  81. }
  82. /**
  83. * Get exit code to be returned when there is a timeout exception
  84. *
  85. * @return int|null
  86. */
  87. public function getIdleTimeoutExitCode()
  88. {
  89. return $this->idleTimeoutExitCode;
  90. }
  91. /**
  92. * @return mixed
  93. */
  94. public function getDeserializer(): callable
  95. {
  96. return $this->deserializer;
  97. }
  98. /**
  99. * @param mixed $deserializer
  100. */
  101. public function setDeserializer(callable $deserializer)
  102. {
  103. $this->deserializer = $deserializer;
  104. }
  105. /**
  106. * @return mixed
  107. */
  108. public function getQos(): array
  109. {
  110. return $this->qos;
  111. }
  112. /**
  113. * @param mixed $qos
  114. */
  115. public function setQos(array $qos)
  116. {
  117. $this->qos = $qos;
  118. }
  119. /**
  120. * @param string $name
  121. */
  122. public function setName(string $name)
  123. {
  124. $this->name = $name;
  125. }
  126. /**
  127. * @return string
  128. */
  129. public function getName(): string
  130. {
  131. return $this->name;
  132. }
  133. /**
  134. * Resets the consumed property.
  135. * Use when you want to call start() or consume() multiple times.
  136. */
  137. public function getConsumed(): int
  138. {
  139. return $this->consumed;
  140. }
  141. /**
  142. * Resets the consumed property.
  143. * Use when you want to call start() or consume() multiple times.
  144. */
  145. public function resetConsumed()
  146. {
  147. $this->consumed = 0;
  148. }
  149. /**
  150. * @return mixed
  151. */
  152. public function getProceedOnException(): bool
  153. {
  154. return $this->proceedOnException;
  155. }
  156. /**
  157. * @param mixed $proceedOnException
  158. */
  159. public function setProceedOnException(bool $proceedOnException)
  160. {
  161. $this->proceedOnException = $proceedOnException;
  162. }
  163. /**
  164. * Consume designated number of messages (0 means infinite)
  165. *
  166. * @param int $msgAmount
  167. *
  168. * @return int
  169. * @throws BadFunctionCallException
  170. * @throws RuntimeException
  171. * @throws AMQPTimeoutException
  172. * @throws ErrorException
  173. */
  174. public function consume($msgAmount = 0): int
  175. {
  176. $this->target = $msgAmount;
  177. $this->setup();
  178. // At the end of the callback execution
  179. while (count($this->getChannel()->callbacks))
  180. {
  181. if ($this->maybeStopConsumer())
  182. {
  183. break;
  184. }
  185. try
  186. {
  187. $this->getChannel()->wait(null, false, $this->getIdleTimeout());
  188. }
  189. catch (AMQPTimeoutException $e)
  190. {
  191. if (null !== $this->getIdleTimeoutExitCode())
  192. {
  193. return $this->getIdleTimeoutExitCode();
  194. }
  195. throw $e;
  196. }
  197. if (!AMQP_WITHOUT_SIGNALS && extension_loaded('pcntl'))
  198. {
  199. pcntl_signal_dispatch();
  200. }
  201. }
  202. return Controller::EXIT_CODE_NORMAL;
  203. }
  204. /**
  205. * Stop consuming messages
  206. */
  207. public function stopConsuming()
  208. {
  209. foreach ($this->queues as $name => $options)
  210. {
  211. $this->getChannel()->basic_cancel($this->getConsumerTag($name), false, true);
  212. }
  213. }
  214. /**
  215. * Force stop the consumer
  216. */
  217. public function stopDaemon()
  218. {
  219. $this->forceStop = true;
  220. $this->stopConsuming();
  221. $this->logger->printInfo("\nConsumer stopped by user.\n");
  222. }
  223. /**
  224. * Force restart the consumer
  225. */
  226. public function restartDaemon()
  227. {
  228. $this->stopConsuming();
  229. $this->renew();
  230. $this->setup();
  231. $this->logger->printInfo("\nConsumer has been restarted.\n");
  232. }
  233. /**
  234. * Sets the qos settings for the current channel
  235. * This method needs a connection to broker
  236. */
  237. protected function setQosOptions()
  238. {
  239. if (empty($this->qos))
  240. {
  241. return;
  242. }
  243. $prefetchSize = $this->qos['prefetch_size'] ?? null;
  244. $prefetchCount = $this->qos['prefetch_count'] ?? null;
  245. $global = $this->qos['global'] ?? null;
  246. $this->getChannel()->basic_qos($prefetchSize, $prefetchCount, $global);
  247. }
  248. /**
  249. * Start consuming messages
  250. *
  251. * @throws RuntimeException
  252. */
  253. protected function startConsuming()
  254. {
  255. $this->id = $this->generateUniqueId();
  256. foreach ($this->queues as $queue => $callback)
  257. {
  258. $that = $this;
  259. $this->getChannel()->basic_consume(
  260. $queue,
  261. $this->getConsumerTag($queue),
  262. null,
  263. null,
  264. null,
  265. null,
  266. function (AMQPMessage $msg) use ($that, $queue, $callback)
  267. {
  268. // Execute user-defined callback
  269. $that->onReceive($msg, $queue, $callback);
  270. }
  271. );
  272. }
  273. }
  274. /**
  275. * Decide whether it's time to stop consuming
  276. *
  277. * @throws BadFunctionCallException
  278. */
  279. protected function maybeStopConsumer(): bool
  280. {
  281. if (extension_loaded('pcntl') && (defined('AMQP_WITHOUT_SIGNALS') ? !AMQP_WITHOUT_SIGNALS : true))
  282. {
  283. if (!function_exists('pcntl_signal_dispatch'))
  284. {
  285. throw new BadFunctionCallException(
  286. "Function 'pcntl_signal_dispatch' is referenced in the php.ini 'disable_functions' and can't be called."
  287. );
  288. }
  289. pcntl_signal_dispatch();
  290. }
  291. if ($this->forceStop || ($this->consumed === $this->target && $this->target > 0))
  292. {
  293. $this->stopConsuming();
  294. return true;
  295. }
  296. if (0 !== $this->getMemoryLimit() && $this->isRamAlmostOverloaded())
  297. {
  298. $this->stopConsuming();
  299. return true;
  300. }
  301. return false;
  302. }
  303. /**
  304. * Callback that will be fired upon receiving new message
  305. *
  306. * @param AMQPMessage $msg
  307. * @param $queueName
  308. * @param $callback
  309. *
  310. * @return bool
  311. * @throws Throwable
  312. */
  313. protected function onReceive(AMQPMessage $msg, string $queueName, callable $callback): bool
  314. {
  315. $timeStart = microtime(true);
  316. \Yii::$app->rabbitmq->trigger(
  317. RabbitMQConsumerEvent::BEFORE_CONSUME,
  318. new RabbitMQConsumerEvent(
  319. [
  320. 'message' => $msg,
  321. 'consumer' => $this,
  322. ]
  323. )
  324. );
  325. try
  326. {
  327. // deserialize message back to initial data type
  328. if ($msg->has('application_headers') &&
  329. isset($msg->get('application_headers')->getNativeData()['rabbitmq.serialized']))
  330. {
  331. $msg->setBody(call_user_func($this->deserializer, $msg->getBody()));
  332. }
  333. // process message and return the result code back to broker
  334. $processFlag = $callback($msg);
  335. $this->sendResult($msg, $processFlag);
  336. \Yii::$app->rabbitmq->trigger(
  337. RabbitMQConsumerEvent::AFTER_CONSUME,
  338. new RabbitMQConsumerEvent(
  339. [
  340. 'message' => $msg,
  341. 'consumer' => $this,
  342. ]
  343. )
  344. );
  345. $this->logger->printResult($queueName, $processFlag, $timeStart);
  346. $this->logger->log(
  347. 'Queue message processed.',
  348. $msg,
  349. [
  350. 'queue' => $queueName,
  351. 'processFlag' => $processFlag,
  352. 'timeStart' => $timeStart,
  353. 'memory' => true,
  354. ]
  355. );
  356. }
  357. catch (Throwable $e)
  358. {
  359. $this->logger->logError($e, $msg);
  360. if (!$this->proceedOnException)
  361. {
  362. throw $e;
  363. }
  364. }
  365. $this->consumed++;
  366. return true;
  367. }
  368. /**
  369. * Mark message status based on return code from callback
  370. *
  371. * @param AMQPMessage $msg
  372. * @param $processFlag
  373. */
  374. protected function sendResult(AMQPMessage $msg, $processFlag)
  375. {
  376. // true in testing environment
  377. if (!isset($msg->delivery_info['channel']))
  378. {
  379. return;
  380. }
  381. // respond to the broker with appropriate reply code
  382. if ($processFlag === ConsumerInterface::MSG_REQUEUE || false === $processFlag)
  383. {
  384. // Reject and requeue message to RabbitMQ
  385. $msg->delivery_info['channel']->basic_reject($msg->delivery_info['delivery_tag'], true);
  386. }
  387. elseif ($processFlag === ConsumerInterface::MSG_REJECT)
  388. {
  389. // Reject and drop
  390. $msg->delivery_info['channel']->basic_reject($msg->delivery_info['delivery_tag'], false);
  391. }
  392. else
  393. {
  394. // Remove message from queue only if callback return not false
  395. $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
  396. }
  397. }
  398. /**
  399. * Checks if memory in use is greater or equal than memory allowed for this process
  400. *
  401. * @return boolean
  402. */
  403. protected function isRamAlmostOverloaded(): bool
  404. {
  405. return memory_get_usage(true) >= ($this->getMemoryLimit() * 1024 * 1024);
  406. }
  407. /**
  408. * @param string $queueName
  409. *
  410. * @return string
  411. */
  412. protected function getConsumerTag(string $queueName): string
  413. {
  414. return sprintf('%s-%s-%s', $queueName, $this->name, $this->id);
  415. }
  416. /**
  417. * @return string
  418. */
  419. protected function generateUniqueId(): string
  420. {
  421. return uniqid('rabbitmq_', true);
  422. }
  423. protected function setup()
  424. {
  425. $this->resetConsumed();
  426. if ($this->autoDeclare)
  427. {
  428. $this->routing->declareAll();
  429. }
  430. $this->setQosOptions();
  431. $this->startConsuming();
  432. }
  433. }