Mq.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php
  2. namespace App\Http\Api;
  3. use App\Tools\Tools;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Str;
  6. use PhpAmqpLib\Connection\AMQPStreamConnection;
  7. use PhpAmqpLib\Exception\AMQPProtocolChannelException;
  8. use PhpAmqpLib\Message\AMQPMessage;
  9. class Mq
  10. {
  11. private static function connection()
  12. {
  13. $host = config('queue.connections.rabbitmq.host');
  14. $port = config('queue.connections.rabbitmq.port');
  15. $user = config('queue.connections.rabbitmq.user');
  16. $password = config('queue.connections.rabbitmq.password');
  17. $vhost = config('queue.connections.rabbitmq.password');
  18. if (empty($host) || empty($port) || empty($user) || empty($password) || empty($vhost)) {
  19. Log::error('rabbitmq set error');
  20. return;
  21. }
  22. $connection = new AMQPStreamConnection($host, $port, $user, $password, $vhost);
  23. return $connection;
  24. }
  25. public static function publish(string $queue, $message)
  26. {
  27. // 一对一
  28. try {
  29. Log::debug('mq publish', ['queue' => $queue, 'message' => $message]);
  30. $host = config('queue.connections.rabbitmq.host');
  31. $port = config('queue.connections.rabbitmq.port');
  32. $user = config('queue.connections.rabbitmq.user');
  33. $password = config('queue.connections.rabbitmq.password');
  34. $vhost = config('queue.connections.rabbitmq.virtual_host');
  35. if (empty($host) || empty($port) || empty($user) || empty($password) || empty($vhost)) {
  36. Log::error('rabbitmq set error');
  37. return;
  38. }
  39. $connection = new AMQPStreamConnection($host, $port, $user, $password, $vhost);
  40. $channel = $connection->channel();
  41. // $channel->queue_declare($queue, false, true, false, false);
  42. $msgId = Str::uuid();
  43. Log::info("mq push message queue={$queue} id={$msgId}");
  44. $msg = new AMQPMessage(
  45. json_encode($message, JSON_UNESCAPED_UNICODE),
  46. [
  47. 'message_id' => $msgId,
  48. 'content_type' => 'application/json; charset=utf-8',
  49. ]
  50. );
  51. $channel->basic_publish($msg, '', $queue);
  52. $channel->close();
  53. $connection->close();
  54. } catch (\Exception $e) {
  55. Log::error($e);
  56. return;
  57. }
  58. }
  59. /**
  60. * 阻塞消费一个已存在的队列。
  61. *
  62. * - 队列必须事先建好(`php artisan app:create-queue`),不存在就记日志并抛异常退出,worker 不再自建队列。
  63. * - 手动 ack、prefetch 1:每条消息处理结束无论成功失败一律 ack,不重试、不进死信队列。
  64. * - 处理满 `mint.mq.consumer_loop_limit` 条(0 = 不限)或检测到 `.stop` 文件后退出。
  65. *
  66. * @param callable(mixed $data, ?string $messageId): int|null $callback 返回非 0 视为失败(只记日志)
  67. *
  68. * @throws \RuntimeException 队列不存在
  69. */
  70. public static function worker(string $queue, ?callable $callback = null): void
  71. {
  72. $consumerTag = '';
  73. $host = config('queue.connections.rabbitmq.host');
  74. $port = config('queue.connections.rabbitmq.port');
  75. $user = config('queue.connections.rabbitmq.user');
  76. $password = config('queue.connections.rabbitmq.password');
  77. $vhost = config('queue.connections.rabbitmq.virtual_host');
  78. $connection = new AMQPStreamConnection($host, $port, $user, $password, $vhost);
  79. $channel = $connection->channel();
  80. // passive:只检查不创建。不存在时服务端返回 404 并关闭 channel
  81. try {
  82. $channel->queue_declare($queue, true);
  83. } catch (AMQPProtocolChannelException $e) {
  84. Log::error("mq worker: queue {$queue} not found", ['error' => $e->getMessage()]);
  85. $connection->close();
  86. throw new \RuntimeException("mq queue [{$queue}] does not exist, run `php artisan app:create-queue` first", 0, $e);
  87. }
  88. $loopLimit = (int) config('mint.mq.consumer_loop_limit', 0);
  89. $processed = 0;
  90. $process_message = function (AMQPMessage $message) use ($callback, $queue, $loopLimit, &$processed) {
  91. $messageId = $message->has('message_id') ? $message->get('message_id') : null;
  92. Log::debug('received message', [
  93. 'message_id' => $messageId,
  94. 'content_type' => $message->has('content_type') ? $message->get('content_type') : null,
  95. ]);
  96. try {
  97. if ($callback !== null) {
  98. $result = $callback(json_decode($message->getBody()), $messageId);
  99. Log::debug('mq done', ['message_id' => $messageId]);
  100. if ($result !== 0) {
  101. throw new \Exception('task error');
  102. }
  103. }
  104. } catch (\Throwable $e) {
  105. Log::error("mq worker {$queue} exception", [
  106. 'queue' => $queue,
  107. 'message_id' => $messageId,
  108. 'exception' => $e,
  109. ]);
  110. } finally {
  111. $message->ack();
  112. }
  113. $processed++;
  114. if ($loopLimit > 0 && $processed >= $loopLimit) {
  115. Log::info("mq exit queue={$queue} loop={$processed}");
  116. $message->getChannel()->basic_cancel($message->getConsumerTag());
  117. } elseif (Tools::isStop()) {
  118. Log::info('mq worker: .stop file exist. cancel the consumer.');
  119. $message->getChannel()->basic_cancel($message->getConsumerTag());
  120. }
  121. };
  122. $channel->basic_qos(null, 1, false);
  123. // no_ack = false:手动 ack
  124. $channel->basic_consume($queue, $consumerTag, false, false, false, false, $process_message);
  125. try {
  126. $channel->consume();
  127. } catch (\Throwable $exception) {
  128. Log::error($exception->getMessage());
  129. }
  130. $channel->close();
  131. $connection->close();
  132. Log::info("mq worker stopped queue={$queue} processed={$processed}");
  133. }
  134. }