mahavirnahata/stream-bus 问题修复 & 功能扩展

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

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

mahavirnahata/stream-bus

Composer 安装命令:

composer require mahavirnahata/stream-bus

包简介

Redis-backed cross-language stream bus for Laravel.

README 文档

README

A Redis-backed cross-language stream bus for Laravel. Publish from any language; consume in Laravel (or vice versa) with at-least-once or effectively-once delivery.

Requirements

  • PHP 8.2+
  • Laravel 11 or 12
  • Redis 5.0+ (Redis 6.2+ for PEL recovery via XAUTOCLAIM)
  • phpredis extension or predis/predis 2.x

Installation

composer require mahavirnahata/stream-bus

Publish the config file:

php artisan vendor:publish --tag=stream-bus-config

Quick start

Publish a message:

use MahavirNahata\StreamBus\Facades\StreamBus;

StreamBus::publish('events:outbound', [
    'type' => 'image.process',
    'payload' => ['id' => 123],
]);

Create a handler:

namespace App\Handlers;

use MahavirNahata\StreamBus\Contracts\StreamBusHandler;

class ImageResultHandler implements StreamBusHandler
{
    public function handle(array $message): void
    {
        // $message['payload'] contains your data
        dispatch(new ProcessImageResult($message['payload'] ?? []));
    }
}

Start consuming:

php artisan stream-bus:consume events:inbound App\Handlers\ImageResultHandler --group=laravel

Drivers

Streams (streams) Lists (lists)
Redis primitive XADD / XREADGROUP RPUSH / BLPOP
Ordering FIFO, guaranteed FIFO, guaranteed
Consumer groups Yes No
At-least-once Yes (PEL + ACK) Yes (pop is destructive)
Multi-consumer Yes, independent Yes, competing
PEL recovery Yes (XAUTOCLAIM) N/A
Cross-language Any client that speaks Redis Streams Any client that speaks Redis Lists

Use streams (default) when you need consumer groups, per-message ACK, and PEL recovery after crashes.

Use lists for simpler competing-consumer queues where Redis Streams features are not required.

Configuration

// config/stream-bus.php
return [
    'driver'           => env('STREAM_BUS_DRIVER', 'streams'),
    'connection'       => env('STREAM_BUS_REDIS', 'default'),
    'prefix'           => env('STREAM_BUS_PREFIX', 'stream-bus:'),
    'cluster'          => env('STREAM_BUS_CLUSTER', false),
    'delivery'         => env('STREAM_BUS_DELIVERY', 'at-least-once'),
    'dedupe_ttl'       => env('STREAM_BUS_DEDUPE_TTL', 86400),
    'maxlen'           => env('STREAM_BUS_MAXLEN', null),
    'reclaim'          => env('STREAM_BUS_RECLAIM', false),
    'min_idle_time'    => env('STREAM_BUS_MIN_IDLE_TIME', 60000),
    'reclaim_count'    => env('STREAM_BUS_RECLAIM_COUNT', 10),
    'max_attempts'     => env('STREAM_BUS_MAX_ATTEMPTS', 0),
    'dead_letter_topic'=> env('STREAM_BUS_DEAD_LETTER_TOPIC', null),
    'consumers'        => [],
];

Config reference

Key Default Description
driver streams streams or lists
connection default Laravel Redis connection name
prefix stream-bus: Key prefix for all topics
cluster false Wrap topic in {} hash tags for Redis Cluster slot colocation
delivery at-least-once at-least-once or effectively-once
dedupe_ttl 86400 Dedup / attempt key TTL in seconds
maxlen null Max stream entries; null = unlimited. Set this in production.
reclaim false Enable automatic PEL recovery (Redis 6.2+)
min_idle_time 60000 ms a PEL message must be idle before reclaim
reclaim_count 10 Max messages to reclaim per loop
max_attempts 0 Max handler attempts before dead-lettering (0 = unlimited)
dead_letter_topic null DLQ topic; defaults to {original}:dead-letter
consumers [] Topic → handler map (see below)

Consumers config

'consumers' => [
    // Simple form
    'events:inbound' => App\Handlers\ImageResultHandler::class,

    // Per-topic overrides
    'events:orders' => [
        'handler'           => App\Handlers\OrderHandler::class,
        'driver'            => 'streams',
        'group'             => 'order-workers',
        'count'             => 5,
        'delivery'          => 'effectively-once',
        'dead_letter_topic' => 'events:orders:dead-letter',
    ],
],

Publishing messages

// Facade
use MahavirNahata\StreamBus\Facades\StreamBus;
StreamBus::publish('events:outbound', ['foo' => 'bar']);

// Helper function
stream_bus()->publish('events:outbound', ['foo' => 'bar']);

// Injected class
use MahavirNahata\StreamBus\StreamBus;
public function __construct(private StreamBus $bus) {}
$this->bus->publish('events:outbound', ['foo' => 'bar']);

Per-call option overrides:

StreamBus::publish('events:outbound', $payload, [
    'driver'     => 'lists',
    'connection' => 'redis-secondary',
    'prefix'     => 'app1:bus:',
]);

Consumer command

php artisan stream-bus:consume [topic] [handler] [options]

Arguments

Argument Description
topic Topic to consume (optional if consumers is configured)
handler Handler class name (required when topic is given)

Options

Option Default Description
--driver config streams or lists
--connection config Redis connection name
--prefix config Key prefix
--group default Consumer group (streams)
--consumer hostname Consumer name (streams)
--count 1 Messages per read (streams)
--block 2000 Block time: ms for streams, seconds for lists
--delivery config at-least-once or effectively-once
--dedupe-ttl config Dedup key TTL in seconds
--once Read once and exit
--sleep 200 Sleep ms between polls when idle
--no-ack Skip ACK (streams)
--stop-on-error Exit if the handler throws
--max-attempts config Max attempts before dead-lettering (0 = unlimited)
--dead-letter-topic config Override DLQ topic name
--memory 128 Exit when process memory exceeds this MB limit
--reclaim config Enable PEL reclaim each loop (Redis 6.2+)
--min-idle-time 60000 ms before a PEL message is eligible for reclaim

Delivery semantics

at-least-once (default)

Messages are processed at least once. Duplicate delivery is possible after a crash. Use idempotent handlers.

effectively-once

A Redis SET NX EX key is written on first processing. Duplicates within dedupe_ttl seconds are skipped.

php artisan stream-bus:consume events:inbound App\Handlers\Foo \
    --delivery=effectively-once \
    --dedupe-ttl=3600

Dead-letter queue

When max_attempts is set and a handler fails that many times, the message is published to the dead-letter topic and ACKed (streams) or discarded (lists).

STREAM_BUS_MAX_ATTEMPTS=3
STREAM_BUS_DEAD_LETTER_TOPIC=events:dead

Or per command:

php artisan stream-bus:consume events:inbound App\Handlers\Foo \
    --max-attempts=3 \
    --dead-letter-topic=events:dead

Consume dead-letter messages like any other topic:

php artisan stream-bus:consume events:dead App\Handlers\DeadLetterInspector

PEL recovery (streams only)

When a consumer crashes while processing, its messages sit unacknowledged in Redis's Pending Entry List (PEL) forever without intervention. Enable automatic reclaim to re-queue them:

STREAM_BUS_RECLAIM=true
STREAM_BUS_MIN_IDLE_TIME=60000   # 60 seconds idle before reclaiming
STREAM_BUS_RECLAIM_COUNT=10      # max reclaimed per loop

Or via CLI:

php artisan stream-bus:consume --reclaim --min-idle-time=60000

Requires Redis 6.2+ (uses XAUTOCLAIM internally).

Redis Cluster

All related keys (stream, dedupe, attempts) must land on the same hash slot. Enable hash-tag wrapping:

STREAM_BUS_CLUSTER=true

With cluster=true, the topic events:outbound produces keys like stream-bus:{events:outbound}, stream-bus:{events:outbound}:dedupe:{id}, etc. — all guaranteed to the same slot.

Stream length management

Redis Streams grow indefinitely without trimming. Set a limit in production:

STREAM_BUS_MAXLEN=100000

Trimming uses the ~ approximate modifier (O(1)). Requires phpredis; on predis the package falls back to a separate XTRIM call (logged as a warning if that also fails).

Metrics

Retrieve live stream / queue health metrics:

$metrics = stream_bus()->metrics('events:outbound', 'default');

// streams result:
// [
//   'driver'  => 'streams',
//   'topic'   => 'events:outbound',
//   'key'     => 'stream-bus:events:outbound',
//   'group'   => 'default',
//   'length'  => 1500,   // total entries in stream
//   'pending' => 3,      // delivered but not yet ACKed
// ]

// lists result:
// [
//   'driver' => 'lists',
//   'topic'  => 'events:outbound',
//   'key'    => 'stream-bus:events:outbound',
//   'length' => 42,
// ]

A value of -1 indicates the Redis command is unavailable for the current client version.

Production deployment

Supervisor

[program:stream-bus-inbound]
command=php /var/www/artisan stream-bus:consume events:inbound App\Handlers\ImageResultHandler
    --group=laravel
    --memory=128
    --reclaim
    --max-attempts=3
autostart=true
autorestart=true
stopwaitsecs=10
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/stream-bus-inbound.log

Graceful shutdown

The consume command installs POSIX signal handlers (SIGTERM, SIGINT, SIGHUP) when the pcntl extension is available. On signal receipt, the current batch completes and the process exits cleanly. The default --block=2000 means the worst-case shutdown delay is ~2 seconds.

Reduce --block further if you need faster shutdown response (e.g. for Kubernetes with a short grace period).

Memory management

The --memory=128 flag exits the process when RSS exceeds the limit, allowing Supervisor to restart it. This prevents slow memory leaks from accumulating indefinitely.

Shared Redis

If you share Redis with other workloads, isolate the bus with a unique prefix or a dedicated Redis connection:

STREAM_BUS_PREFIX=myapp:bus:
STREAM_BUS_REDIS=stream-bus-connection

Cross-language interoperability

The wire format for every message is:

{
  "id": "<uuid-v4>",
  "ts": 1716000000,
  "payload": { "your": "data" }
}

See examples/ for producers and consumers in Node.js, Python, and Go.

Go (streams)

examples/go-producer/main.go   — publish via XADD
examples/go-consumer/main.go   — consume via XREADGROUP / XACK

Run each independently:

cd examples/go-producer && go mod init producer && go get github.com/redis/go-redis/v9 github.com/google/uuid && go run main.go
cd examples/go-consumer && go mod init consumer && go get github.com/redis/go-redis/v9 && go run main.go

Node.js (streams)

examples/node-producer.js   — publish via xAdd
examples/node-consumer.js   — consume via xReadGroup / xAck
npm install redis
node examples/node-producer.js
node examples/node-consumer.js

Python (streams / lists)

examples/python-producer.py   — publish via xadd
examples/python-consumer.py   — consume via xreadgroup / xack
pip install redis
python examples/python-producer.py
python examples/python-consumer.py

Troubleshooting

Consumer exits immediately with "Handler class not found"
Ensure the handler class exists, is autoloaded, and implements StreamBusHandler.

No messages received
Verify topic name and prefix match between publisher and consumer. For streams, confirm the group name matches.

Duplicate messages
Expected with at-least-once. Switch to effectively-once with idempotent handlers, or use max_attempts=1 with a dead-letter queue.

Stream grows unbounded
Set STREAM_BUS_MAXLEN in your .env.

Shutdown takes too long
Reduce --block (e.g. --block=500). The maximum shutdown delay equals the block time.

PEL grows after consumer crashes
Enable --reclaim or set STREAM_BUS_RECLAIM=true. Requires Redis 6.2+.

FAQ

Does it scan all Redis keys?
No — it reads only the configured topic key with your prefix.

Can I run multiple consumers for the same topic?
Yes. With the streams driver, consumers in the same group share the load. With lists, multiple consumers compete naturally.

Exactly-once delivery?
Not guaranteed. Use effectively-once with idempotent handlers for the best-effort equivalent.

Can I use this without Laravel?
The core StreamBus class only depends on Illuminate\Contracts\Redis\Factory. You can wire it up manually in any container.

License

MIT

mahavirnahata/stream-bus 适用场景与选型建议

mahavirnahata/stream-bus 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 0 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 02 月 10 日, 在 PHP 生态内属于活跃度较高的组件。

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

围绕 mahavirnahata/stream-bus 我们能提供哪些服务?
定制开发 / 二次开发

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

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

GitHub 信息

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

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-02-10