babelqueue

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 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.

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 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

This section is empty.

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) 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 InMemoryTransport added in v0.2.0

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

InMemoryTransport is an in-process Transport for tests and broker-free local runs. It is safe for concurrent use. Pop returns immediately (it does not block for the timeout) — when the queue is empty it returns (nil, nil).

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) 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
}

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

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
kafka module
machinery module
otel module
pulsar module
redis module
sqs module

Jump to

Keyboard shortcuts

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