tsterker/hopper 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

tsterker/hopper

Composer 安装命令:

composer require tsterker/hopper

包简介

Opinionated RabbitMQ client.

README 文档

README

CI

tsterker/hopper

Opinionated RabbitMQ client (Hopper) intended to facilitate the implementation of "at least once" semantics bundled with a RabitMQ Management Interface client (Warren).

🚧 This library (and this README) is the rushed and incoherent result out of "brainstorming" and "coding by wishful thinking" session with very specific use-cases in mind and is bound to change or even be completely reimagined in a later iteration.

Some guiding principles/thoughts were:

  • Pragmatic, purpose-focused; keeping interface minimal/simple
  • Facilitate "at least once" semantics, also for chains of consumers, where incoming messages should only be ACKed after publish of outgoing message was confirmed
  • Make it easy to send regular heartbeats (e.g. by leveraging declare(ticks=N))
  • Don't try to generalize the solution yet (e.g. currently only support for fanout exchanges), but get a feeling for a general architecture/core concepts.
  • Don't leak underlying AMQP library (too much), but internally fully commit to it.
  • Consider extending library to also support AMQP libraries (bunny?) or switch to it all together. Main requirement is that publisher confirms are supported.

Some opinions, which might be loosened as the package matures:

  • Enforce prefetch count
  • Use publisher confirms
  • Only declare durable queues/exchanges
  • Only send persistent messages (delivery_mode=persistent)
  • Only use lazy queues (x-queue-mode=lazy)
  • Only raw json messages
  • Only declare FANOUT exchanges
  • ...

1. Table of Contents

2. Getting Started

⚠️ The library currently depends on an AMQPStreamConnection. The goal is to move this dependency inside Hopper.

Create an AMQP connection.

$connection = AMQPLazyConnection::create_connection(
    [
        ['host' => 'localhost', 'port' => 5672, 'user' => 'user', 'password' => 'pass', 'vhost' => '/'],
    ],
    [
        'keepalive' => true,
        'heartbeat' => 60,
        'connection_timeout' => 5,
        'read_write_timeout' => null,
    ]
);

Start using hopper

use TSterker\Hopper\Hopper;
use TSterker\Hopper\Message;

$hopper = new Hopper($connection);

$fooExchange = $hopper->createExchange('foo');
$fooQueue = $hopper->createQueue('foo');

// Declare exchange & bind queue to it
$hopper->declareExchange($fooExchange);
$hopper->declareQueue($fooQueue);
$hopper->bind($fooExchange, $fooQueue);

// Subscribe to queue
$hopper->subscribe($fooQueue function (Message $msg, Hopper $hopper): void {
    echo "Message {$msg->getId()} received: " . json_encode($msg->getData()) . "\n";
    $msg->ack();
});

// Publish single message
$hopper->publish($fooExchange, Message::make(['foo' => 'bar']));

// Publish message batch
$hopper->publishBatch($fooExchange, [
    Message::make(['bar' => 'baz']),
    Message::make(['baz' => 'bazinga']),
]);

// Buffer messages & batch publish
$hopper->addBatchMessage($fooExchange, Message::make(['batch' => '1']));
$hopper->addBatchMessage($fooExchange, Message::make(['batch' => '1']));
$hopper->flushBatchPublishes();

// Consume messages for 5 seconds
$hopper->consume(5);

3. Core Concepts

Below a rought outline of the core concepts that govern this library and how they are intended to be leveraged for different use cases.

3.1. Subscriber

 * "LOW LEVEL" (e.g. not Handler classes, plain callbacks. But convenience of idle handling)
 * - Register one or more message handler callback(s) to queue(s)
 * - Register idle handler
 * - Consume
  • Subscribe to multiple queues
  • Register idle handler that should be called when no messages are received for configured idle timeout
use TSterker\Hopper\Subscriber;
use TSterker\Hopper\Hopper;
use TSterker\Hopper\Message;

/** @var Hopper $hopper */

$subscriber = new Subscriber($hopper);

// Plumbing
$source = $hopper->createExchange('source');
$fooQueue = $hopper->createQueue('foo');
$barQueue = $hopper->createQueue('bar');
$hopper->declareExchange($source);
$hopper->declareQueue($fooQueue);
$hopper->declareQueue($barQueue);
$hopper->bind($source, $fooQueue);
$hopper->bind($source, $barQueue);

$subscriber
    ->subscribe($fooQueue, function (Message $msg) {
        echo "FOO Subscriber: {$msg->getId()}\n";
        $msg->ack();
    })
    ->subscribe($barQueue, function (Message $msg) {
        echo "BAR Subscriber: {$msg->getId()}\n";
        $msg->ack();
    })
    ->withIdleTimeout(1)  // Call idle handler after 1 second of not receiving messages
    ->useIdleHandler(function ($timeout) {
        echo "idle for more than $timeout seconds...\n";
    });

$subscriber->consume();

3.3. Piper

The Piper builds on top of Subscribers and supports "at least once" semantics in message pipelines, by ensuring that each incoming message is only ACKed after an outgoing message was confirmed.

  • Connect input and output queue with a Tranformer
  • Transformer receives message from input queue and returns a message that should be published to output queue
  • Piper will take care of ACKing incoming messages only once outgoing messages were successfully published
  • Supports message buffering & batch publish
  • TODO: Don't mix concepts of onFlush and idle callbacks?
use TSterker\Hopper\Piper;
use TSterker\Hopper\Hopper;

/** @var Hopper $hopper */
class ExampleTransformer implements \TSterker\Hopper\Contracts\Transformer
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function transformMessage(Message $msg): Message
    {
        return Message::make(['name' => $this->name]);
    }
}

// Plumbing
$source = $hopper->createExchange('source');
$fooQueue = $hopper->createQueue('foo');
$barQueue = $hopper->createQueue('bar');
$outQueue = $hopper->createQueue('out');
$hopper->declareExchange($source);
$hopper->declareQueue($fooQueue);
$hopper->declareQueue($barQueue);
$hopper->declareQueue($outQueue);
$hopper->bind($source, $fooQueue);
$hopper->bind($source, $barQueue);

$piper = new Piper(
    $hopper,
    10,  // buffer messages and then batch-publish
    2,   // "idle timeout" in seconds to flush buffer, even if not full
);

$fooX = new ExampleTransformer('foo');
$barX = new ExampleTransformer('bar');

$piper
    ->add($fooQueue, $outQueue, $fooX)
    ->add($barQueue, $outQueue, $barX)
    ->onFlush(function (int $messageCount): void {
        echo "Flushed $messageCount messages\n";
    });

$piper->consume();

4. Optimization / Parameter Tuning

This secion is a brain-dump of things to consider for potential optimizations.

4.1. Optimize Message Publishing

see https://github.com/php-amqplib/php-amqplib#optimized-message-publishing

4.2. Publisher Confirms

4.2.1. ACK latency for persisted messages

see https://www.rabbitmq.com/confirms.html#publisher-confirms-latency

basic.ack for a persistent message routed to a durable queue will be sent after persisting the message to disk. The RabbitMQ message store persists messages to disk in batches after an interval (a few hundred milliseconds) to minimise the number of fsync(2) calls, or when a queue is idle.

This means that under a constant load, latency for basic.ack can reach a few hundred milliseconds. To improve throughput, applications are strongly advised to process acknowledgements asynchronously (as a stream) or publish batches of messages and wait for outstanding confirms. The exact API for this varies between client libraries.

5. Troubleshooting

5.1. Framing error

"Framing error, unexpected byte: 4f"
  • Seems I only ran into this when attempting to wait for pending publisher acknowledgements (wait_for_pending_acks).
  • Seems to happen when the read_write_timeout is too low (e.g. 0-10) when creating a connection
  • Setting read_write_timeout to null (or ommiting it) seems to prevent this error, but might come with implications if rabbitmq would stop responding?
  • Currently I settled with setting it to a reasonably high 120. If I would run into such a timeout, I'd be happy for the process to fail.
$connection = AMQPLazyConnection::create_connection(
    [$host],
    [
        'keepalive' => true,
        'heartbeat' => env('RABBITMQ_HEARTBEAT', 60),
        'connection_timeout' => env('RABBITMQ_CONNECTION_TIMEOUT', 5),
        'read_write_timeout' => 0,  // <-- Setting this to 0 consistently reproduces the error
    ]
);

6. TODO

  • Allow more configuration for what types of queues/exchanges to declare
  • ...

tsterker/hopper 适用场景与选型建议

tsterker/hopper 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1.43k 次下载、GitHub Stars 达 0, 最近一次更新时间为 2020 年 07 月 21 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 tsterker/hopper 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 tsterker/hopper 我们能提供哪些服务?
定制开发 / 二次开发

基于 tsterker/hopper 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1.43k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2020-07-21