babelqueue

package module
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 21, 2026 License: MIT Imports: 9 Imported by: 10

README

BabelQueue for Go

CI Go Reference Go Report Card

Polyglot Queues, Simplified. Read and write the canonical BabelQueue message envelope from Go — so your Go services exchange messages with Laravel, Symfony, Python, .NET and Node over one strict JSON format, on the broker you already run.

This is the framework-agnostic Go core: the wire-envelope codec, contracts and dead-letter helpers — zero dependencies (standard library only). The full standard is documented at babelqueue.com.

Installation

go get github.com/babelqueue/babelqueue-go

Requires Go >=1.21.

Usage

package main

import (
	"fmt"

	babelqueue "github.com/babelqueue/babelqueue-go"
)

func main() {
	// Produce — build the canonical envelope and publish the JSON to your broker.
	env, err := babelqueue.Make(
		"urn:babel:orders:created",
		map[string]any{"order_id": 1042},
		babelqueue.WithQueue("orders"),
	)
	if err != nil {
		panic(err)
	}
	body, _ := env.Encode() // []byte of compact UTF-8 JSON
	// redisClient.RPush(ctx, "queues:orders", body)
	//   /  ch.PublishWithContext(ctx, "", "orders", false, false, amqp.Publishing{Body: body})

	// Consume — decode a message produced by ANY BabelQueue SDK.
	in, err := babelqueue.Decode(body)
	if err != nil || !in.Accepts() {
		return // malformed or unsupported — dead-letter / drop
	}
	switch in.URN() {
	case "urn:babel:orders:created":
		fmt.Println(in.Data["order_id"], in.TraceID) // 1042, the cross-service trace id
	}
}

The envelope is identical to every other SDK's:

{
  "job": "urn:babel:orders:created",
  "trace_id": "…",
  "data": { "order_id": 1042 },
  "meta": { "id": "…", "queue": "orders", "lang": "go", "schema_version": 1, "created_at": 1749132727000 },
  "attempts": 0
}

Encode disables HTML escaping and emits compact JSON — the same canonical wire form as the PHP and Python cores (the envelope frame is byte-identical). Key order inside your data map follows encoding/json (sorted), where PHP/Python keep insertion order — JSON object key order is insignificant, so consumers read them identically. JSON numbers decode into Data as float64 (encoding/json's default for any).

Trace continuation

Make mints a fresh trace_id unless you pass one — propagate an inbound trace across a hop with WithTraceID:

next, _ := babelqueue.Make(
	"urn:babel:shipping:requested",
	map[string]any{"order_id": 1042},
	babelqueue.WithTraceID(in.TraceID),
)
Dead-letter
dl := babelqueue.Annotate(env, "failed", "orders", 3, err) // additive dead_letter block
body, _ := dl.Encode()
// publish body to the "orders.dlq" queue

Annotate returns a copy — the original envelope is preserved unchanged inside the dead-lettered message, so any-language consumers can still read it.

Runtime — produce & consume

The core is just the codec. An optional, still-zero-dependency runtime (App) ties it to a broker through a small Transport interface, with URN routing, attempts-based retry → dead-letter, and unknown-URN strategies:

import babelqueue "github.com/babelqueue/babelqueue-go"

app := babelqueue.NewApp(transport, // a Transport (see below)
    babelqueue.WithDefaultQueue("orders"),
    babelqueue.WithMaxAttempts(3),
    babelqueue.WithDeadLetter(true),
)

app.Handle("urn:babel:orders:created", func(ctx context.Context, env babelqueue.Envelope) error {
    // ... handle env.Data; return an error to retry / dead-letter
    return nil
})

app.Publish(ctx, "urn:babel:orders:created", map[string]any{"order_id": 1042})
app.Consume(ctx) // blocks; routes by URN until ctx is cancelled

InMemoryTransport (in the core) backs tests and local runs with zero deps. Broker drivers live in separate modules, so the core itself never pulls a dependency:

go get github.com/babelqueue/babelqueue-go/redis   # Redis  (go-redis)
go get github.com/babelqueue/babelqueue-go/amqp    # RabbitMQ (amqp091-go)
import (
    babelqueue "github.com/babelqueue/babelqueue-go"
    "github.com/babelqueue/babelqueue-go/redis"
)

tr, _ := redis.New("redis://localhost:6379/0")        // reliable BLMOVE + processing list
app := babelqueue.NewApp(tr, babelqueue.WithDefaultQueue("orders"))

The RabbitMQ transport (amqp.New("amqp://…")) publishes to a durable queue with persistent delivery and the contract AMQP properties (type=URN, correlation_id=trace_id, x-schema-version/x-source-lang/x-attempts), and consumes with basic.get + manual ack. Implement babelqueue.Transport yourself to back the runtime with any other broker.

Idempotent consumption (dedupe on meta.id)

BabelQueue is at-least-once, so handlers should dedupe on the envelope's meta.id (ADR-0022). The idempotency subpackage (in the core, zero-dep) makes that a one-line wrap: a message whose id was already processed is skipped instead of re-run.

import "github.com/babelqueue/babelqueue-go/idempotency"

store := idempotency.NewInMemoryStore() // process-local reference store
app.Handle("urn:babel:orders:created", idempotency.Wrap(store, func(ctx context.Context, env babelqueue.Envelope) error {
    // runs at most once per meta.id; a thrown error leaves the id unmarked so retry/DLQ still apply
    return nil
}))

InMemoryStore is for tests / single-process consumers. A fleet of consumers needs a shared, persistent Store — two backends ship as separate submodules (so the core stays zero-dependency; a driver is pulled in only if you import them):

go get github.com/babelqueue/babelqueue-go/idempotency-redis      # Redis    (go-redis)
go get github.com/babelqueue/babelqueue-go/idempotency-postgres   # Postgres (database/sql + pgx)
import (
    "github.com/babelqueue/babelqueue-go/idempotency"
    redisstore "github.com/babelqueue/babelqueue-go/idempotency-redis"
)

store, _ := redisstore.New("redis://localhost:6379/0", redisstore.WithTTL(24*time.Hour))
defer store.Close()
app.Handle("urn:babel:orders:created", idempotency.Wrap(store, handler))

Both persistent backends implement the same idempotency.Store interface as the in-memory reference, with an atomic claim so concurrent consumers serialize on meta.id — Redis via SET key val NX PX <ttl>, Postgres via INSERT … ON CONFLICT … RETURNING on the primary key (DDL via schema.sql / Store.Migrate). A duplicate delivery is detected as already-seen; an optional WithTTL expires ids so they may be re-processed once the window lapses. See each submodule's README for details.

Transactional outbox (atomic write + relayed publish)

A plain producer makes a dual write — commit the business row and publish to the broker — two independent systems that disagree on a crash. The outbox subpackage (in the core, zero-dep) removes it (ADR-0029): the message is persisted into the same database, in the same transaction, as the business data, so it commits or rolls back atomically with it; a separate relay publishes the durable rows afterwards. It is the producer-side counterpart to consumer-side idempotency above.

The caller owns the transaction boundary — Outbox.Write does not begin or commit anything; it just encodes the envelope (frozen codec, bytes unchanged) and hands it to a Store the caller has bound to their open transaction:

import "github.com/babelqueue/babelqueue-go/outbox"

ob := outbox.New(store) // store is YOUR DB-bound outbox.Store (tx-scoped)

// inside the transaction you already opened around the business write:
insertOrder(ctx, tx, order)                  // the business write
env, _ := babelqueue.Make("urn:babel:orders:created", data, babelqueue.WithQueue("orders"))
ob.Write(env)                                // same tx, via a tx-bound Store
tx.Commit()                                  // both, or neither

The outbox.Store interface is the only thing you implement — the core pulls in no DB driver (GR-7). It stores the Encoded envelope verbatim and the relay publishes those exact bytes, so trace_id is preserved end-to-end (GR-4) and every SDK stays byte-compatible (GR-5):

type Store interface {
    Save(encoded []byte, queue string) (id string, err error)
    FetchUnpublished(limit int) ([]outbox.Record, error) // SHOULD claim rows (FOR UPDATE SKIP LOCKED)
    MarkPublished(ids []string) error
    MarkFailed(id, reason string) error
}

A separate relay drains the committed rows through the same Transport seam, on a worker loop or a cron:

relay := outbox.NewRelay(transport, store, outbox.Options{}) // zero Options = sane defaults

relay.Flush(ctx)      // publish one batch; mark each row published AFTER publish returns
relay.Drain(ctx, 0)   // loop Flush until a pass makes no progress (safety ceiling)

A row is marked published only after the transport accepts it — a crash in between re-publishes it next pass (at-least-once; the consumer dedupes on meta.id). A publish error is caught, the row is marked failed (with a bounded linear backoff) and left pending, so one poison row never blocks the batch. outbox.InMemoryStore is a process-local reference for tests / single-process demos (no real transaction — use a DB-backed adapter in production).

What this core is

It enforces the contract: the envelope shape, URN identity, trace propagation, schema-version gating and the dead-letter block — with zero dependencies. The optional App runtime and InMemoryTransport are zero-dep too; only the Redis and RabbitMQ transport modules pull a broker driver, and only if you import them.

Unknown-URN strategy constants (StrategyFail, StrategyDelete, StrategyRelease, StrategyDeadLetter) drive the runtime's unknown-URN handling.

Conformance

This core passes the shared cross-SDK conformance suite (vendored under testdata/conformance/) — the same fixtures every BabelQueue SDK must satisfy, so a Go producer and, say, a Laravel consumer agree byte-for-byte.

go test ./...

License

MIT © Muhammet Şafak

Documentation

Overview

Package babelqueue is the framework-agnostic Go core of BabelQueue — "Polyglot Queues, Simplified".

It implements the canonical wire envelope so a Go service interoperates byte-for-byte with the PHP/Laravel, Python, ... SDKs over any broker, with no language-specific serialization on the wire:

env, _ := babelqueue.Make("urn:babel:orders:created", map[string]any{"order_id": 1042})
body, _ := env.Encode() // publish body to Redis / RabbitMQ / ...

in, _ := babelqueue.Decode(body)
if in.Accepts() {
	switch in.URN() {
	case "urn:babel:orders:created":
		// handle in.Data ...
	}
}

The package depends only on the standard library.

Full spec: https://babelqueue.com

Index

Constants

View Source
const (
	StrategyFail       = "fail"        // surface an error; let the worker decide
	StrategyDelete     = "delete"      // drop the message
	StrategyRelease    = "release"     // requeue for another consumer
	StrategyDeadLetter = "dead_letter" // route to the dead-letter queue
)

Unknown-URN strategies: what a consumer does with a message whose URN has no registered handler. These mirror the constants in every other SDK core.

View Source
const HeaderReplayBypass = "bq-replay-bypass"

HeaderReplayBypass is the out-of-band transport header Redrive stamps (when its options set Bypass) on a replayed message, and that the App runtime surfaces to the handler as IsReplay. It lets a handler skip external side-effects that already ran, with no change to the frozen envelope (ADR-0027).

View Source
const SchemaVersion = 1

SchemaVersion is the wire envelope schema version this core implements. It is versioned independently of the module's release (package) version: the wire format only changes when this number does.

View Source
const SourceLang = "go"

SourceLang is stamped into meta.lang for envelopes produced by this core.

Variables

View Source
var (
	// ErrEmptyURN is returned by Make when the URN is blank — a polyglot message
	// must expose a stable, non-empty URN so consumers can identify it without
	// relying on any language-specific class name.
	ErrEmptyURN = errors.New("babelqueue: a polyglot message must expose a stable, non-empty URN")

	// ErrUnknownURN signals that no handler is mapped for a message URN.
	ErrUnknownURN = errors.New("babelqueue: no handler is mapped for the message URN")
)

Sentinel errors returned by the core.

Functions

func BypassExternalEffects added in v1.5.0

func BypassExternalEffects(ctx context.Context, fn func() error) error

BypassExternalEffects runs fn unless the current message is a replay (see IsReplay), in which case fn is skipped and nil is returned. Wrap the external, non-idempotent side of a handler — sending an email, charging a card, calling a third party — so a replay re-runs the idempotent core but does not re-fire effects that already happened:

app.Handle("urn:babel:orders:created", func(ctx context.Context, env babelqueue.Envelope) error {
	saveOrder(env)                       // idempotent core — always runs
	return babelqueue.BypassExternalEffects(ctx, func() error {
		return sendConfirmationEmail(env) // external effect — skipped on replay
	})
})

func HeadersFromContext added in v1.6.0

func HeadersFromContext(ctx context.Context) map[string]string

HeadersFromContext returns the out-of-band transport headers that arrived with the message currently being handled, or nil when none were carried (or the transport does not surface headers). The returned map is read-only — do not mutate it. It is the consume-side counterpart of HeaderPublisher: a handler or an optional wrapper (e.g. the otel module's WrapHandler) reads per-message metadata that travels beside the frozen envelope, never in it (GR-1).

func IsReplay added in v1.5.0

func IsReplay(ctx context.Context) bool

IsReplay reports whether the message currently being handled was redriven with the replay-bypass marker set — i.e. this is a deliberate replay, and external side-effects that already happened should be skipped. It reads the marker the runtime put on the context from the HeaderReplayBypass transport header.

Types

type App added in v0.2.0

type App struct {
	// contains filtered or unexported fields
}

App is the optional BabelQueue runtime: it produces and consumes polyglot messages over a Transport. Routing is by URN; the wire format is the canonical envelope (via the same core codec), so it interoperates with the PHP/Laravel, Python, ... SDKs. Retry uses the top-level attempts counter; failures past MaxAttempts go to a dead-letter queue when enabled.

The core codec (Make/[Encode]/Decode) has zero dependencies; the App adds no dependencies either (broker drivers live in separate modules). An App is safe for concurrent Publish calls; run Consume from a single goroutine per queue.

func NewApp added in v0.2.0

func NewApp(transport Transport, opts ...AppOption) *App

NewApp builds a runtime over the given transport.

func (*App) Consume added in v0.2.0

func (a *App) Consume(ctx context.Context, queue ...string) error

Consume processes messages from the default queue (or the optional override) until ctx is cancelled. It blocks; run it in its own goroutine. A single bad message never stops the loop — it is retried or dead-lettered.

func (*App) Drain added in v0.2.0

func (a *App) Drain(ctx context.Context, queue string, max int) (int, error)

Drain processes up to max messages from queue and returns the count, stopping early when the queue is empty. With max <= 0 it drains until empty. Useful for tests and one-shot workers.

func (*App) Handle added in v0.2.0

func (a *App) Handle(urn string, handler Handler)

Handle registers handler as the consumer for urn (the last registration wins).

func (*App) Publish added in v0.2.0

func (a *App) Publish(ctx context.Context, urn string, data map[string]any, opts ...Option) (string, error)

Publish builds the canonical envelope for (urn, data) and publishes it. It returns the message id (meta.id). Pass envelope options such as WithQueue or WithTraceID to override the target queue or continue a trace.

func (*App) PublishWithHeaders added in v1.6.0

func (a *App) PublishWithHeaders(
	ctx context.Context,
	urn string,
	data map[string]any,
	headers map[string]string,
	opts ...Option,
) (string, error)

PublishWithHeaders builds the canonical envelope for (urn, data) and publishes it together with out-of-band transport headers, returning the message id (meta.id). The headers ride beside the frozen envelope on the transport's per-message metadata channel (e.g. a W3C traceparent for cross-hop span linkage, ADR-0028) — the envelope itself is never touched (GR-1).

It is the produce-side counterpart of the headers the runtime surfaces to a handler via HeadersFromContext. When the transport implements HeaderPublisher the headers are propagated; otherwise it transparently falls back to a plain publish (the headers are dropped, exactly as Redrive degrades — no error, no regression), so callers need not branch on transport capability. Passing nil or empty headers is equivalent to App.Publish.

func (*App) Run added in v0.2.0

func (a *App) Run(ctx context.Context) error

Run is an alias for Consume on the default queue.

type AppOption added in v0.2.0

type AppOption func(*App)

AppOption customizes NewApp.

func WithDeadLetter added in v0.2.0

func WithDeadLetter(enabled bool) AppOption

WithDeadLetter enables routing exhausted/failed messages to a dead-letter queue.

func WithDeadLetterQueue added in v0.2.0

func WithDeadLetterQueue(queue string) AppOption

WithDeadLetterQueue overrides the dead-letter queue name (default: the source queue plus the dead-letter suffix).

func WithDeadLetterSuffix added in v0.2.0

func WithDeadLetterSuffix(suffix string) AppOption

WithDeadLetterSuffix sets the suffix appended to the source queue to derive the dead-letter queue name (default ".dlq").

func WithDefaultQueue added in v0.2.0

func WithDefaultQueue(queue string) AppOption

WithDefaultQueue sets the queue used by Publish/Consume when none is given (default "default").

func WithMaxAttempts added in v0.2.0

func WithMaxAttempts(n int) AppOption

WithMaxAttempts sets how many delivery attempts a message gets before it is dead-lettered or dropped (default 3).

func WithPollTimeout added in v0.2.0

func WithPollTimeout(d time.Duration) AppOption

WithPollTimeout sets how long Pop blocks waiting for a message each iteration (default 1s). Ignored by transports that do not block.

func WithUnknownURNStrategy added in v0.2.0

func WithUnknownURNStrategy(strategy string) AppOption

WithUnknownURNStrategy sets what happens to a message whose URN has no handler: one of StrategyFail, StrategyDelete, StrategyRelease, StrategyDeadLetter.

type DeadLetter

type DeadLetter struct {
	Reason        string  `json:"reason"`
	Error         *string `json:"error"`     // null when no cause was supplied
	Exception     *string `json:"exception"` // null when no cause was supplied
	FailedAt      int64   `json:"failed_at"` // Unix milliseconds, UTC
	OriginalQueue string  `json:"original_queue"`
	Attempts      int     `json:"attempts"`
	Lang          string  `json:"lang"`
}

DeadLetter is the additive block appended to an envelope when a message is dead-lettered. The original envelope is preserved unchanged alongside it, so a consumer in any language can still read the original job, data and trace_id.

type Envelope

type Envelope struct {
	Job        string         `json:"job"`      // the message URN (never a class name)
	TraceID    string         `json:"trace_id"` // correlation id, preserved across hops
	Data       map[string]any `json:"data"`     // the message payload
	Meta       Meta           `json:"meta"`
	Attempts   int            `json:"attempts"`              // top-level transport retry counter
	DeadLetter *DeadLetter    `json:"dead_letter,omitempty"` // present only once dead-lettered
}

Envelope is the canonical BabelQueue wire message: a strict, language-neutral JSON shape ({job, trace_id, data, meta, attempts}) that every SDK produces and consumes identically. The field order below is significant — it matches the other cores so the envelope frame encodes identically across languages. (Key order inside the user-supplied Data map is JSON-insignificant; encoding/json emits it sorted, where PHP/Python preserve insertion order — same JSON.)

func Annotate

func Annotate(env Envelope, reason, originalQueue string, attempts int, cause error) Envelope

Annotate returns a copy of the envelope with a dead_letter block attached, recording why and where it failed; it does not mutate the original. A non-nil cause fills in the error message and its Go type; both serialize as JSON null when cause is nil.

func Decode

func Decode(raw []byte) (Envelope, error)

Decode parses a raw JSON body into an Envelope. It accepts "urn" as an inbound alias for "job" (resolving it into Job). It does not validate the contents — use Accepts for consumer-side validation.

func Make

func Make(urn string, data map[string]any, opts ...Option) (Envelope, error)

Make builds the canonical envelope for a (urn, data) pair. It mints a fresh trace id unless WithTraceID is given, starts attempts at 0, and stamps meta with a unique id, the source language ("go"), the schema version and a millisecond timestamp. It returns ErrEmptyURN when urn is blank.

func (Envelope) Accepts

func (e Envelope) Accepts() bool

Accepts reports whether a consumer should accept this envelope. It rejects a missing URN, an unsupported meta.schema_version, a blank trace_id, or missing data — the consumer-side counterpart to the producer JSON Schema. (It accepts the urn alias, unlike the stricter producer schema.)

func (Envelope) Encode

func (e Envelope) Encode() ([]byte, error)

Encode renders the envelope as compact UTF-8 JSON with HTML escaping disabled (unescaped slashes and unicode) — the canonical wire form shared by every SDK. The envelope frame is identical across languages; key order within the Data map follows encoding/json (sorted), which is semantically the same JSON.

func (Envelope) URN

func (e Envelope) URN() string

URN returns the message URN — the canonical job, with the urn alias already resolved by Decode.

type Handler added in v0.2.0

type Handler func(ctx context.Context, env Envelope) error

Handler processes one decoded message. Returning an error triggers the retry / dead-letter path; returning nil acknowledges the message.

type HeaderPublisher added in v1.5.0

type HeaderPublisher interface {
	PublishWithHeaders(ctx context.Context, queue, body string, headers map[string]string) error
}

HeaderPublisher is an optional Transport capability: publish a body together with out-of-band transport headers (e.g. the replay-bypass marker), for brokers that carry per-message metadata. A transport that does not implement it simply does not propagate headers — callers fall back to plain Publish (ADR-0027).

type InMemoryTransport added in v0.2.0

type InMemoryTransport struct {
	// contains filtered or unexported fields
}

func NewInMemoryTransport added in v0.2.0

func NewInMemoryTransport() *InMemoryTransport

NewInMemoryTransport returns an empty in-process transport.

func (*InMemoryTransport) Ack added in v0.2.0

Ack is a no-op: Pop already removed the message.

func (*InMemoryTransport) Pop added in v0.2.0

Pop removes and returns the head of queue, or (nil, nil) when it is empty.

func (*InMemoryTransport) Publish added in v0.2.0

func (t *InMemoryTransport) Publish(_ context.Context, queue, body string) error

Publish appends body to the in-memory queue.

func (*InMemoryTransport) PublishWithHeaders added in v1.5.0

func (t *InMemoryTransport) PublishWithHeaders(_ context.Context, queue, body string, headers map[string]string) error

PublishWithHeaders appends body plus out-of-band headers (HeaderPublisher).

func (*InMemoryTransport) Size added in v0.2.0

func (t *InMemoryTransport) Size(queue string) int

Size reports how many messages are pending on queue (test helper).

type Meta

type Meta struct {
	ID            string `json:"id"`
	Queue         string `json:"queue"`
	Lang          string `json:"lang"`
	SchemaVersion int    `json:"schema_version"`
	CreatedAt     int64  `json:"created_at"` // Unix milliseconds, UTC
}

Meta is the immutable per-message metadata block.

type Option

type Option func(*makeConfig)

Option customizes Make.

func WithQueue

func WithQueue(queue string) Option

WithQueue sets the logical queue name recorded in meta.queue (default "default").

func WithTraceID

func WithTraceID(traceID string) Option

WithTraceID reuses an existing trace id (trace continuation) instead of minting a fresh one. A blank value is ignored.

type ReceivedMessage added in v0.2.0

type ReceivedMessage struct {
	Body   string
	Queue  string
	Handle any
	// Headers are out-of-band transport headers a [HeaderPublisher] carried with the
	// message (e.g. the bq-replay-bypass marker). Nil for transports that don't surface
	// them; reads are nil-safe.
	Headers map[string]string
}

ReceivedMessage is a message reserved from a queue, plus a transport-internal handle the transport uses to acknowledge it.

type RedriveItem added in v1.4.0

type RedriveItem struct {
	MessageID string // meta.id ("" if absent or undecodable)
	TraceID   string
	URN       string
	Reason    string // dead_letter.reason
	From      string // the DLQ it was read from
	To        string // target queue (the plan, even on a dry run; "" when skipped/undecodable)
	Redriven  bool   // true only when actually re-published to To
	Bypassed  bool   // true when the bq-replay-bypass header was stamped on the redriven message
}

RedriveItem records what happened to one message during a Redrive run.

type RedriveOptions added in v1.4.0

type RedriveOptions struct {
	// ToQueue overrides where messages are re-published. When empty, each message goes back
	// to its own dead_letter.original_queue; set it to a sandbox queue to replay safely.
	ToQueue string
	// Max caps how many messages are pulled from the DLQ (0 = all currently available).
	Max int
	// DryRun inspects without redriving: every message is read, reported, and returned to the
	// DLQ unchanged — nothing is re-published to a source/sandbox queue.
	DryRun bool
	// Select, when non-nil, picks which messages to redrive (e.g. by reason or URN).
	// Unselected messages are returned to the DLQ unchanged.
	Select func(Envelope) bool
	// Bypass stamps the bq-replay-bypass transport header on each redriven message, so a
	// handler can skip external side-effects that already ran (see [BypassExternalEffects]).
	// It takes effect only when the transport is a [HeaderPublisher]; otherwise it is a no-op
	// and the per-item Bypassed flag stays false (ADR-0027).
	Bypass bool
	// Timeout is the per-pop wait passed to the transport (default 1s).
	Timeout time.Duration
}

RedriveOptions configures a Redrive run.

type RedriveResult added in v1.4.0

type RedriveResult struct {
	Redriven int
	Skipped  int
	Items    []RedriveItem
}

RedriveResult summarizes a Redrive run.

func Redrive added in v1.4.0

func Redrive(ctx context.Context, t Transport, dlq string, opts RedriveOptions) (RedriveResult, error)

Redrive moves dead-lettered messages off the dlq queue and re-publishes each to its source queue (its dead_letter.original_queue) or to opts.ToQueue, resetting it for reprocessing: the dead_letter block is removed and attempts reset to 0, while job, trace_id, data and meta are preserved verbatim. It is the operator-side counterpart to the runtime's dead-letter routing — the contract leaves redrive to tooling, and this is that tool (ADR-0026).

Messages are drained from the DLQ first and then processed, so restored messages (skipped, dry-run, or undecodable) are never re-encountered in the same run. A DLQ message is acknowledged only after its re-publish succeeds; an undecodable body is restored, not lost.

type Transport added in v0.2.0

type Transport interface {
	// Publish appends an already-encoded envelope to queue.
	Publish(ctx context.Context, queue, body string) error
	// Pop reserves the next message from queue, blocking up to timeout. It returns
	// (nil, nil) when no message arrives before the timeout.
	Pop(ctx context.Context, queue string, timeout time.Duration) (*ReceivedMessage, error)
	// Ack acknowledges (removes) a previously reserved message.
	Ack(ctx context.Context, msg *ReceivedMessage) error
}

Transport is the minimal broker contract the App runtime talks to: publish a raw (already-encoded) body, reserve the next message, and acknowledge it. The runtime is broker-agnostic — implement this to back it with any broker. The github.com/babelqueue/babelqueue-go/redis and .../amqp modules provide Redis and RabbitMQ implementations; InMemoryTransport backs tests and local runs.

Directories

Path Synopsis
amqp module
artemis module
asynq module
Package idempotency adds optional, dependency-free dedupe to a babelqueue consumer: it wraps a babelqueue.Handler so a message whose meta.id was already processed is skipped instead of run again (ADR-0022).
Package idempotency adds optional, dependency-free dedupe to a babelqueue consumer: it wraps a babelqueue.Handler so a message whose meta.id was already processed is skipped instead of run again (ADR-0022).
kafka module
machinery module
otel module
Package outbox adds an optional, dependency-free transactional outbox to a babelqueue producer (ADR-0029): the message is persisted into the same database, in the same transaction, as the business data — so it commits or rolls back atomically with it — and a separate Relay publishes the durable rows afterwards.
Package outbox adds an optional, dependency-free transactional outbox to a babelqueue producer (ADR-0029): the message is persisted into the same database, in the same transaction, as the business data — so it commits or rolls back atomically with it — and a separate Relay publishes the durable rows afterwards.
pulsar module
redis module
Package schema adds optional, dependency-free per-URN payload validation to a babelqueue producer or consumer (ADR-0024).
Package schema adds optional, dependency-free per-URN payload validation to a babelqueue producer or consumer (ADR-0024).
sqs module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL