Documentation
¶
Overview ¶
Package redis is a Redis-backed babelqueue.Transport for the BabelQueue Go runtime. It uses the reliable-queue pattern: RPUSH to produce, BLMOVE the head to a per-queue processing list to reserve (so an in-flight message survives a worker crash), and LREM from that list to acknowledge.
tr, _ := redis.New("redis://localhost:6379/0")
app := babelqueue.NewApp(tr, babelqueue.WithDefaultQueue("orders"))
This default layout is a Go-owned reliable queue. For a *shared* queue with Laravel workers, WithLaravelLayout switches the key names and the reservation to the broker-bindings.md §1.1 layout (see "Laravel-compatible layout" below).
It also implements the optional babelqueue.HeaderPublisher capability: out-of-band transport headers (e.g. a W3C traceparent for cross-hop span linkage, ADR-0028, or the bq-replay-bypass marker, ADR-0027) ride beside the frozen envelope (GR-1) — never in it — and are surfaced back to the consumer on babelqueue.ReceivedMessage.Headers.
Redis stores only the raw list value (the LREM ack handle *is* that value), so — unlike AMQP headers or SQS MessageAttributes — there is no native per-message metadata channel. To carry headers the transport owns a tiny JSON *frame* distinct from the wire envelope:
{"__bq_frame":1,"headers":{"traceparent":"00-..."},"body":"<raw wire envelope>"}
RPUSH stores the frame, so the LREM ack handle stays byte-for-byte what was pushed and the reliable-queue semantics (RPUSH/BLMOVE/LREM/processing list) are untouched. Framing is opt-in and backward compatible: only PublishWithHeaders with a non-empty map writes a frame; plain Publish (and PublishWithHeaders with no headers) stores the bare envelope byte-for-byte, exactly as before. Pop detects frame-vs-bare by the reserved "__bq_frame" sentinel — a frozen wire envelope can never carry it — and a bare value consumes with Headers=nil and Handle=value, so cross-version queues interoperate.
Laravel-compatible layout ¶
WithLaravelLayout puts the transport on the key names of broker-bindings.md §1.1, `<prefix><name>` (ready list), `:delayed` and `:reserved` (sorted sets) and `:notify` (blocking-pop wake-up), with `queues:` as the Laravel default prefix. Transport.Keys returns them. Publish pushes to the ready list and signals the notify list.
Pop first migrates due `:delayed` members and then expired `:reserved` members back to the ready list (score <= floor(now)), then runs Laravel's pop script: it LPOPs the head, decodes it with cjson (under pcall), increments `attempts`, writes the re-encoded member to `:reserved` scored floor(now)+retry_after and consumes one notify token. The handler receives the raw head byte for byte; the attempt count it sees is job.attempts+1 (DeliveryCount). Ack is ZREM of the `:reserved` member. A head that cjson cannot decode, or whose `attempts` is not a number, is written to `:reserved` raw and delivered as poison (A1): the App writes it to the DLQ and acknowledges it, so nothing is lost and nothing stays in `:reserved`.
The re-encoded `:reserved` member is the documented Laravel compatibility exception (ADR-0033) to the lossless re-emit rule of ADR-0031: only the Laravel pop script touches the payload with cjson. The SDK itself always re-emits through its codec, which preserves unknown keys.
Transport.Release (A2) moves the reservation to `:delayed` in one script — ZREM from `:reserved`, ZADD to `:delayed` at floor(now)+ceil(delay) with the SDK-encoded body (attempts+1, unknown keys preserved). Transport.PublishDelayed schedules a message the same way (no notify token: the migrate writes one per moved item). A reservation that is never acknowledged is recovered once its deadline passes and counts as an attempt; retry_after defaults to 90 s, WithoutRetryAfter disables recovery and a negative WithRetryAfter is a configuration error. Every blocking wait is sliced to at most one second with a migrate before each slice, so a delayed message that falls due while a consumer waits is delivered within one second.
There is no out-of-band header channel in this layout (A3): PublishWithHeaders drops the headers and writes the bare envelope, so a traceparent is not carried through Redis lists here (trace_id stays in the envelope, GR-4). A "__bq_frame" value is never written.
Default (legacy) layout recovery ¶
The default `<queue>` + `<queue>:processing` layout keeps its names. Each reservation also writes a deadline member `<token>:<raw>` (token: 32 lowercase hex from crypto/rand) to the `<queue>:processing:deadlines` sorted set, scored floor(now)+retry_after (+inf when WithoutRetryAfter). A sweep before every reserve moves expired members back to the ready list (body and attempts unchanged); ack, release and dead-letter are gated on removing the reservation's own deadline member, so a late ack can never delete another consumer's copy. Items without a deadline member (reserved by an older version) are never touched.
Index ¶
- Constants
- Variables
- type Keys
- type Option
- type Transport
- func (t *Transport) Ack(ctx context.Context, msg *babelqueue.ReceivedMessage) error
- func (t *Transport) Close() error
- func (t *Transport) DeadLetter(ctx context.Context, msg *babelqueue.ReceivedMessage, dlqQueue, body string) error
- func (t *Transport) Keys(queue string) Keys
- func (t *Transport) Pop(ctx context.Context, queue string, timeout time.Duration) (*babelqueue.ReceivedMessage, error)
- func (t *Transport) Publish(ctx context.Context, queue, body string) error
- func (t *Transport) PublishDelayed(ctx context.Context, queue string, env babelqueue.Envelope, ...) error
- func (t *Transport) PublishWithHeaders(ctx context.Context, queue, body string, headers map[string]string) error
- func (t *Transport) Release(ctx context.Context, msg *babelqueue.ReceivedMessage, delay time.Duration) error
Constants ¶
const DefaultKeyPrefix = "queues:"
DefaultKeyPrefix is Laravel's default Redis queue key prefix (`queues:`).
const DefaultMigrationBatchSize = -1
DefaultMigrationBatchSize means "move every due member" (-1), the Laravel behaviour.
const DefaultRetryAfter = 90 * time.Second
DefaultRetryAfter is the reservation window (Laravel's `retry_after` default, 90 s): a reserved member whose deadline has passed is made ready again.
Variables ¶
var ErrDelayedUnsupported = errors.New("babelqueue/redis: delayed publish needs the Laravel-compatible layout")
ErrDelayedUnsupported is returned by delayed publishing and release in the default layout, which has no delayed set; use WithLaravelLayout.
var ErrInvalidRetryAfter = errors.New("babelqueue/redis: retry_after must not be negative")
ErrInvalidRetryAfter reports a negative retry_after (a configuration error, §1.7).
Functions ¶
This section is empty.
Types ¶
type Keys ¶ added in v1.2.0
type Keys struct {
Ready string // list
Delayed string // sorted set, scored by available-at
Reserved string // sorted set, scored by the retry-after deadline
Notify string // list, blocking-pop wake-up
}
Keys are the Redis key names of one logical queue in the Laravel-compatible layout (broker-bindings.md §1.1).
func LaravelKeys ¶ added in v1.2.0
LaravelKeys returns the §1.1 key names for queue under prefix.
type Option ¶
type Option func(*Transport)
Option customizes New / NewWithClient.
func WithLaravelLayout ¶ added in v1.2.0
WithLaravelLayout switches the transport to the Laravel-compatible key layout of broker-bindings.md §1.1 under prefix (empty means DefaultKeyPrefix). The default layout (a `<queue>` list plus a `<queue>:processing` list) is unchanged when this option is not used.
func WithMigrationBatchSize ¶ added in v1.3.0
WithMigrationBatchSize bounds how many members one migrate (Laravel layout) or sweep (default layout) call moves; -1 (the default, DefaultMigrationBatchSize) moves them all. Zero or a value below -1 is ignored.
func WithProcessingSuffix ¶
WithProcessingSuffix overrides the per-queue processing-list suffix (default ":processing").
func WithRetryAfter ¶ added in v1.2.0
WithRetryAfter sets the reservation window (default DefaultRetryAfter). In the Laravel layout an expired :reserved member is made ready again and counts as an attempt; in the default layout an expired deadline member is swept back to the ready list. A negative value is a configuration error (ErrInvalidRetryAfter); use WithoutRetryAfter to disable recovery.
func WithoutRetryAfter ¶ added in v1.3.0
func WithoutRetryAfter() Option
WithoutRetryAfter is the "retry_after: null" setting: this consumer never recovers expired :reserved members (Laravel layout) and its legacy deadline members score +inf, so it never sweeps its own reservations (it still sweeps other consumers' expired members).
type Transport ¶
type Transport struct {
// contains filtered or unexported fields
}
Transport implements babelqueue.Transport over Redis.
func NewWithClient ¶
NewWithClient wraps an existing go-redis client (bring your own pool/TLS/auth). A configuration error from an option (a negative retry_after) is reported by every operation.
func (*Transport) Ack ¶
func (t *Transport) Ack(ctx context.Context, msg *babelqueue.ReceivedMessage) error
Ack acknowledges a reservation. Laravel layout: ZREM of the :reserved member. Default layout: the gated ack — remove the reservation's own deadline member and, only if that removal succeeded, exactly one copy of the processing item; an expired reservation is a no-op that logs a warning, so a late ack never deletes another consumer's copy.
func (*Transport) DeadLetter ¶ added in v1.3.0
func (t *Transport) DeadLetter(ctx context.Context, msg *babelqueue.ReceivedMessage, dlqQueue, body string) error
DeadLetter writes body to dlqQueue and drops the reservation in one gated step (babelqueue.DeadLetterer, §1.7). Default layout: behind the deadline-member gate shared with Ack and Release. Laravel layout: one script that ZREMs the reserved member and, only when that removed it, pushes body (and a notify token) to the dead-letter queue. A consumer whose reservation expired writes nothing and logs a warning, so the message is never both dead-lettered and handled by its new owner.
func (*Transport) Keys ¶ added in v1.2.0
Keys returns the key names the transport uses for queue. In the Laravel layout (WithLaravelLayout) these are the §1.1 names; in the default layout Ready is the queue list itself, Reserved is its processing list, and Delayed and Notify are empty (unused).
func (*Transport) Pop ¶
func (t *Transport) Pop(ctx context.Context, queue string, timeout time.Duration) (*babelqueue.ReceivedMessage, error)
Pop reserves the next message of queue, waiting up to timeout. timeout <= 0 means "wait until a message arrives or ctx ends"; a positive timeout returns (nil, nil) when nothing arrived in time.
Laravel layout: every iteration migrates due :delayed / expired :reserved members, runs the pop script and, when empty, waits at most one second (BLPOP on :notify, or a sleep for the last sub-second stretch because BLPOP cannot wait less than a second). Default layout: every iteration sweeps expired reservations and runs the non-blocking reserve script; between empty polls it sleeps 100 ms, doubling up to 1 s, reset by a message, never beyond the timeout.
func (*Transport) Publish ¶
Publish appends body to queue (RPUSH). The bare wire envelope is stored byte-for-byte (no frame), so a non-otel publisher and a cross-version consumer interoperate exactly as before.
func (*Transport) PublishDelayed ¶ added in v1.3.0
func (t *Transport) PublishDelayed(ctx context.Context, queue string, env babelqueue.Envelope, delay time.Duration) error
PublishDelayed schedules env on queue for delivery after delay (Laravel layout only): ZADD to `:delayed` with score floor(now)+ceil(delay). No notify token is written — the migrate that moves it writes one (§1.6).
func (*Transport) PublishWithHeaders ¶ added in v1.1.0
func (t *Transport) PublishWithHeaders(ctx context.Context, queue, body string, headers map[string]string) error
PublishWithHeaders appends body together with out-of-band transport headers (babelqueue.HeaderPublisher). In the default layout, when the header map is non-empty the value RPUSHed is a transport-owned frame ({"__bq_frame":1,"headers":…,"body":<raw envelope>}) that carries the headers beside the frozen envelope (GR-1) — e.g. a W3C traceparent for cross-hop span linkage (ADR-0028). Blank keys/values are dropped; if nothing survives (or the map is nil/empty) it degrades to a byte-identical bare Transport.Publish.
In the Laravel-compatible layout (A3) no frame is ever written: the headers are dropped and the bare envelope is published, because a framed element would break Laravel workers and the pop script's cjson decode. A traceparent is therefore not carried through that layout; trace_id stays in the envelope (GR-4).
func (*Transport) Release ¶ added in v1.3.0
func (t *Transport) Release(ctx context.Context, msg *babelqueue.ReceivedMessage, delay time.Duration) error
Release returns a reserved message to the queue to be redelivered after delay (babelqueue.Releaser).
Laravel layout (A2): one script removes the reservation from `:reserved` and adds the body to `:delayed` at floor(now)+ceil(delay); the body is the message re-encoded by the SDK codec with attempts = job.attempts+1, unknown keys and their types preserved (ADR-0031). A body that does not decode yields babelqueue.ErrReleaseUnsupported (the App falls back to re-publish + ack).
Default layout: the release is a gated move back to the ready list with attempts+1 (the deadline-member gate of §1.7: a consumer whose reservation expired releases nothing). That layout has no delayed set, so a positive delay is not honoured — the message is re-queued at once, exactly what the App's re-publish fallback did, but now behind the gate so an expired reservation can no longer produce a duplicate. A body that does not decode yields babelqueue.ErrReleaseUnsupported here too: releasing it verbatim would never advance its attempts (this layout has no delivery counter), so the App would redeliver it forever. The App does not normally reach this branch for such a body: Transport.Pop flags it babelqueue.ReceivedMessage.Poison, so the App dead-letters the raw body before any release (binding §1.1); the check stays as defense for a caller that releases a message directly.