Reliability & governance helpers
Last updated:
Beyond produce/consume, the Python core ships optional, opt-in helpers that mechanise the
reliability and governance contracts the wire spec defines. The core stays stdlib-only; the
crypto and OTel references live behind extras. None touch the frozen envelope
(schema_version: 1); each is a tooling layer over the codec. They are the Python face of the
cross-SDK spec — follow the linked spec page for the full contract.
Idempotency
wrap(store, handler) makes a handler run at most once per meta.id, even on
at-least-once redelivery. It keeps the handler’s signature (via functools.wraps):
from babelqueue import BabelQueue, InMemoryStore
from babelqueue.idempotency import wrap
app = BabelQueue("redis://localhost:6379/0", queue="orders")
store = InMemoryStore() # tests / single process
app.register("urn:babel:orders:created", wrap(store, on_order_created))
The in-memory InMemoryStore is the reference; for a fleet, implement the IdempotencyStore
protocol (seen / remember / forget) over a shared backend (no persistent store ships
in core yet — bring your own). See Idempotency and the
store deep-dive.
Transactional outbox
Outbox / OutboxRelay remove the producer dual write: persist the encoded envelope in
the same DB transaction as your business row, then a relay publishes the durable rows
verbatim.
from babelqueue import Outbox, OutboxRelay, InMemoryOutboxStore
store = InMemoryOutboxStore() # production: a DB-backed OutboxStore
outbox = Outbox(store)
# inside YOUR DB transaction, beside the business write — no commit of its own:
row_id = outbox.write(envelope) # encodes via the codec, calls store.save()
# later, a relay drains the durable rows to the broker:
result = OutboxRelay(app.transport, store).drain() # result.published / result.failed
OutboxStore is the four-method protocol (save / fetch_unpublished / mark_published /
mark_failed); the relay forwards through the runtime’s publish-only Transport
(publish(queue, body)). The transaction boundary is yours, and the relay backoff is in
seconds. See Transactional Outbox.
DLQ redrive & replay-bypass
redrive(transport, dlq, ...) moves dead-lettered messages back onto a queue — dead_letter
removed, attempts reset to 0, everything else preserved — with dry_run, select,
to_queue (sandbox) and bypass. The replay guard skips effects that already fired:
from babelqueue import is_replay, bypass_external_effects
from babelqueue.redrive import redrive
# inspect / sandbox-replay, stamping bq-replay-bypass
res = redrive(app.transport, "orders.dlq", to_queue="sandbox", bypass=True)
@app.handler("urn:babel:orders:created")
def on_order_created(data, meta):
save_order(data) # idempotent core — always runs
bypass_external_effects(lambda: send_email(data)) # external effect — skipped on replay
On Kafka, redrive keeps the DLQ record’s key (ReceivedMessage.key) on the write back to the
work topic as well as on every restore write back to the DLQ: a null key stays null and an empty
key stays empty.
is_replay() reads the per-message replay flag the runtime sets (a contextvars flag), so it
takes no argument. See DLQ redrive & replay-bypass.
GDPR field encryption
protect / unprotect encrypt only the data leaves a schema marks x-gdpr-sensitive, in
place. Cipher is a caller-bound protocol; the core ships no concrete cipher (Python’s
stdlib has no AES-GCM) — the AES-256-GCM reference rides the optional babelqueue[gdpr] extra
(cryptography). Validate cleartext — protect after validation on produce, unprotect
before validation on consume.
from babelqueue import protect, unprotect, Cipher, DecryptError
from babelqueue.schema import sensitive_paths
protect(data, schema, cipher) # producer: encrypt marked leaves (cipher implements Cipher)
try:
unprotect(data, schema, cipher) # consumer: inverse
except DecryptError:
raise # wrong key / tampered → retry / dead-letter
OpenTelemetry (traceparent)
The babelqueue.otel module (the babelqueue[otel] extra, so importing the core stays
dependency-free) emits publish <urn> / process <urn> spans and, on a header-carrying
transport, injects/extracts the W3C traceparent so a consumer span is a true child of the
producer span — degrading to v0.1 trace_id correlation otherwise. In-memory, Redis, AMQP and
SQS carry it.
from babelqueue import otel
from opentelemetry import trace
tracer = trace.get_tracer("orders")
app.register("urn:babel:orders:created", otel.wrap_handler(tracer, on_order_created))
otel.publish(tracer, app, "urn:babel:orders:created", {"order_id": 1042})
See Observability.
Per-URN schema validation
validate(provider, urn, data) (producer guard) / wrap(provider, urn, handler) (consumer
safety net) validate a message’s data against the JSON Schema registered for its URN,
bridged from a babelqueue-registry manifest via DirProvider. See
Per-URN schema validation.