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
- Variables
- func BypassExternalEffects(ctx context.Context, fn func() error) error
- func IsReplay(ctx context.Context) bool
- type App
- func (a *App) Consume(ctx context.Context, queue ...string) error
- func (a *App) Drain(ctx context.Context, queue string, max int) (int, error)
- func (a *App) Handle(urn string, handler Handler)
- func (a *App) Publish(ctx context.Context, urn string, data map[string]any, opts ...Option) (string, error)
- func (a *App) Run(ctx context.Context) error
- type AppOption
- func WithDeadLetter(enabled bool) AppOption
- func WithDeadLetterQueue(queue string) AppOption
- func WithDeadLetterSuffix(suffix string) AppOption
- func WithDefaultQueue(queue string) AppOption
- func WithMaxAttempts(n int) AppOption
- func WithPollTimeout(d time.Duration) AppOption
- func WithUnknownURNStrategy(strategy string) AppOption
- type DeadLetter
- type Envelope
- type Handler
- type HeaderPublisher
- type InMemoryTransport
- func (t *InMemoryTransport) Ack(_ context.Context, _ *ReceivedMessage) error
- func (t *InMemoryTransport) Pop(_ context.Context, queue string, _ time.Duration) (*ReceivedMessage, error)
- func (t *InMemoryTransport) Publish(_ context.Context, queue, body string) error
- func (t *InMemoryTransport) PublishWithHeaders(_ context.Context, queue, body string, headers map[string]string) error
- func (t *InMemoryTransport) Size(queue string) int
- type Meta
- type Option
- type ReceivedMessage
- type RedriveItem
- type RedriveOptions
- type RedriveResult
- type Transport
Constants ¶
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.
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).
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.
const SourceLang = "go"
SourceLang is stamped into meta.lang for envelopes produced by this core.
Variables ¶
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
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 IsReplay ¶ added in v1.5.0
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 (*App) Consume ¶ added in v0.2.0
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
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
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.
type AppOption ¶ added in v0.2.0
type AppOption func(*App)
AppOption customizes NewApp.
func WithDeadLetter ¶ added in v0.2.0
WithDeadLetter enables routing exhausted/failed messages to a dead-letter queue.
func WithDeadLetterQueue ¶ added in v0.2.0
WithDeadLetterQueue overrides the dead-letter queue name (default: the source queue plus the dead-letter suffix).
func WithDeadLetterSuffix ¶ added in v0.2.0
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
WithDefaultQueue sets the queue used by Publish/Consume when none is given (default "default").
func WithMaxAttempts ¶ added in v0.2.0
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
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
type Handler ¶ added in v0.2.0
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
func (t *InMemoryTransport) Ack(_ context.Context, _ *ReceivedMessage) error
Ack is a no-op: Pop already removed the message.
func (*InMemoryTransport) Pop ¶ added in v0.2.0
func (t *InMemoryTransport) Pop(_ context.Context, queue string, _ time.Duration) (*ReceivedMessage, error)
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 WithTraceID ¶
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.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
amqp
module
|
|
|
artemis
module
|
|
|
asynq
module
|
|
|
azureservicebus
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). |
|
idempotency-postgres
module
|
|
|
idempotency-redis
module
|
|
|
kafka
module
|
|
|
machinery
module
|
|
|
otel
module
|
|
|
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
|