Documentation
¶
Overview ¶
Package hutch is a broker-agnostic worker-pool for queue consumers in Go.
It separates the orchestration that every queue consumer needs — a pool of workers, a bounded number of in-flight messages, a configurable success/failure (ack/nack) policy, per-message timeouts, and graceful draining on shutdown — from the broker-specific transport. Brokers plug in behind two small interfaces (Subscriber and Producer); ship with RabbitMQ, Redis, and in-memory drivers, and add Kafka/NATS/etc. by implementing the same interfaces.
Why it exists ¶
hutch encodes a set of hard-won production lessons:
- Bounded prefetch is what makes consumers scale horizontally. Without a per-consumer in-flight limit, one replica greedily buffers the whole backlog and newly-added replicas starve. The Pool always sets a prefetch (defaulting to the worker count) so work spreads fairly across every replica.
- Be channel-light. A pool of N workers shares one broker channel/consumer, not one channel per worker — opening a channel per worker exhausts broker channel limits the moment you scale out.
- Isolate publishing from consuming. The RabbitMQ driver publishes over a separate connection so a consumer-side disruption can never starve the producers that your request path depends on.
- Fail predictably. Choose Drop, Requeue, or Reject explicitly; the default (Drop) never lets failed messages pile up as unacked or flood a dead-letter queue.
- Drain on shutdown. Pool.Close stops accepting new work and waits for in-flight handlers to finish, bounded by the context you pass.
Quick start ¶
sub, _ := rabbitmq.NewSubscriber("amqp://guest:guest@localhost:5672/")
pool := hutch.NewPool(sub, hutch.WithLogger(log.Default()))
pool.Handle("orders", hutch.Handle(func(ctx context.Context, o Order) error {
return process(ctx, o)
}), hutch.Workers(20))
defer pool.Close(context.Background())
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("hutch: closed")
ErrClosed is returned by operations on a Pool, Publisher, or driver that has already been closed.
Functions ¶
This section is empty.
Types ¶
type Backoff ¶
type Backoff struct {
Min time.Duration // delay before the first retry
Max time.Duration // cap on the delay
Factor float64 // multiplier per attempt (>= 1)
Jitter bool // randomize within [d/2, d] to avoid thundering herds
}
Backoff describes an exponential backoff schedule with optional jitter, used by drivers when reconnecting to a broker.
func DefaultBackoff ¶
func DefaultBackoff() Backoff
DefaultBackoff returns a sensible schedule: 1s, 2s, 4s … capped at 30s, with jitter.
type ErrorPolicy ¶
type ErrorPolicy int
ErrorPolicy decides what happens to a message when its handler returns an error.
const ( // Drop acknowledges the failed message, removing it (at-most-once). This is // the default: it never lets failures accumulate as unacked messages or // flood a dead-letter queue. Pair it with OnErrorFunc to capture failures. Drop ErrorPolicy = iota // Requeue negatively-acknowledges with redelivery. Use only when failures // are expected to be transient — a permanently-failing ("poison") message // will loop forever, so add your own attempt cap if you choose this. Requeue // Reject negatively-acknowledges without redelivery. On brokers with a // dead-letter destination configured for the queue the message is routed // there; otherwise it is dropped. Reject )
func (ErrorPolicy) String ¶
func (p ErrorPolicy) String() string
type HandlerFunc ¶
HandlerFunc processes a single message. Returning nil acks the message; returning an error applies the queue's ErrorPolicy. The context carries the per-message timeout (see HandlerTimeout) and is cancelled when the pool shuts down, so long-running handlers should respect it.
func Handle ¶
func Handle[T any](fn func(ctx context.Context, v T) error) HandlerFunc
Handle adapts a typed handler into a HandlerFunc by JSON-decoding the message body into T. A decode failure is returned as an error (and so follows the queue's ErrorPolicy — typically Drop, since a malformed payload can never succeed on retry).
pool.Handle("orders", hutch.Handle(func(ctx context.Context, o Order) error {
return process(ctx, o)
}))
type Logger ¶
Logger is the minimal logging surface hutch needs. It is satisfied by the standard library's *log.Logger, so you can pass log.Default() directly. The default is a no-op.
type Message ¶
type Message interface {
// Body returns the raw payload bytes.
Body() []byte
// Ack acknowledges successful processing; the broker removes the message.
Ack() error
// Nack signals failure. If requeue is true the broker should redeliver the
// message; if false it should drop it (or route it to a dead-letter
// destination, where the broker supports one).
Nack(requeue bool) error
}
Message is a single unit of work yielded by a Subscriber. The worker that receives it must eventually call exactly one of Ack or Nack — the Pool does this automatically based on the handler result and the configured ErrorPolicy.
type Option ¶
type Option func(*poolConfig)
Option configures a Pool.
func WithErrorPolicy ¶
func WithErrorPolicy(p ErrorPolicy) Option
WithErrorPolicy sets the default policy applied when a handler errors.
func WithHandlerTimeout ¶
WithHandlerTimeout sets the default per-message handler timeout (0 disables).
func WithLogger ¶
WithLogger sets the logger (default: no-op). *log.Logger satisfies it.
func WithPrefetch ¶
WithPrefetch sets the default max in-flight messages per queue. When unset (or 0) it defaults to the queue's worker count, which keeps every worker busy without hoarding the backlog from other replicas.
func WithWorkers ¶
WithWorkers sets the default number of worker goroutines per queue.
type Pool ¶
type Pool struct {
// contains filtered or unexported fields
}
Pool is a broker-agnostic consumer worker-pool. It draws messages from a Subscriber, fans them out to a configurable number of workers per queue, bounds in-flight work via prefetch, applies an ErrorPolicy on failure, and drains gracefully on shutdown.
A Pool is safe for concurrent use. Create one per Subscriber.
func NewPool ¶
func NewPool(sub Subscriber, opts ...Option) *Pool
NewPool builds a Pool over the given Subscriber.
func (*Pool) Close ¶
Close stops accepting new messages and waits for in-flight handlers to finish, bounded by ctx. Messages still in flight when ctx expires are abandoned and will be redelivered by the broker after restart (at-least-once). Close is idempotent.
func (*Pool) Handle ¶
func (p *Pool) Handle(queue string, h HandlerFunc, opts ...QueueOption) error
Handle starts a worker pool for queue. It may be called for multiple queues on the same Pool. It returns once the subscription is established and workers are running; it does not block.
type Producer ¶
type Producer interface {
Publish(ctx context.Context, queue string, body []byte) error
Close() error
}
Producer is the publish side of a broker driver. Implementations must be safe for concurrent use.
type Publisher ¶
type Publisher struct {
// contains filtered or unexported fields
}
Publisher is a thin, broker-agnostic convenience wrapper over a Producer. It adds JSON encoding; the resilience and batching/pooling live in the driver.
func NewPublisher ¶
NewPublisher wraps a Producer driver.
type QueueOption ¶
type QueueOption func(*queueConfig)
QueueOption overrides pool defaults for a single Pool.Handle call.
func HandlerTimeout ¶
func HandlerTimeout(d time.Duration) QueueOption
HandlerTimeout overrides the per-message timeout for this queue (0 disables).
func OnError ¶
func OnError(p ErrorPolicy) QueueOption
OnError overrides the ErrorPolicy for this queue.
func OnErrorFunc ¶
func OnErrorFunc(fn func(Message, error)) QueueOption
OnErrorFunc registers a callback invoked with every failed message and its error (before the ErrorPolicy is applied) — handy for metrics or capturing dropped payloads.
func Prefetch ¶
func Prefetch(n int) QueueOption
Prefetch overrides the max in-flight messages for this queue.
type Subscriber ¶
type Subscriber interface {
Subscribe(ctx context.Context, queue string, prefetch int) (<-chan Message, error)
Close() error
}
Subscriber is the consume side of a broker driver.
Subscribe delivers messages from queue on the returned channel, keeping at most prefetch messages in flight (delivered but not yet acked) at a time — the mechanism that lets multiple replicas share a queue fairly. A driver is expected to handle its own connection resilience: the returned channel should stay open across transient reconnects and close only when ctx is cancelled or the driver is closed.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
connectors
|
|
|
memory
Package memory is an in-process hutch driver backed by Go channels.
|
Package memory is an in-process hutch driver backed by Go channels. |
|
rabbitmq
Package rabbitmq is a hutch driver for RabbitMQ (AMQP 0-9-1), built on github.com/rabbitmq/amqp091-go.
|
Package rabbitmq is a hutch driver for RabbitMQ (AMQP 0-9-1), built on github.com/rabbitmq/amqp091-go. |
|
redis
Package redis is a hutch driver backed by Redis Streams and consumer groups, built on github.com/redis/go-redis/v9.
|
Package redis is a hutch driver backed by Redis Streams and consumer groups, built on github.com/redis/go-redis/v9. |
|
examples
|
|
|
memory
command
Command memory-example runs a hutch worker pool against the in-memory driver, so it needs no broker.
|
Command memory-example runs a hutch worker pool against the in-memory driver, so it needs no broker. |
|
rabbitmq
command
Command rabbitmq-example wires a hutch worker pool and publisher to RabbitMQ with isolated connections and graceful shutdown.
|
Command rabbitmq-example wires a hutch worker pool and publisher to RabbitMQ with isolated connections and graceful shutdown. |
|
redis
command
Command redis-example wires a hutch worker pool and publisher to Redis Streams with isolated clients and graceful shutdown.
|
Command redis-example wires a hutch worker pool and publisher to Redis Streams with isolated clients and graceful shutdown. |