All notable changes to com.babelqueue:babelqueue-redis are documented here.
The format is based on Keep a Changelog, and
this project adheres to Semantic Versioning.
The envelope wire format is versioned separately by meta.schema_version
(currently 1) — see the contract at babelqueue.com.
- The poison
dead_letterblock is now built by corePoisonRecord.build(body, originalQueue, attempts, error, exception)(babelqueue-core 1.14.0) instead of by hand; the DLQ record is byte-for-byte unchanged (reason: "poison",failed_at,lang: "java"). The localPoisonRecordcopy was removed.
-
Laravel-compatible zset layout (
RedisLayout.ZSET, ADR-0033, conformance 1.6.0). Keysqueues:<q>,:delayed,:reserved,:notify; reserve / release / migrate / dead-letter are single Lua scripts run throughEVALSHAwith anEVALfallback onNOSCRIPT(LuaExecutor); bothcjsoncalls of the reserve script (decode and re-encode) arepcall-guarded, so a body that cannot be decoded or re-encoded (e.g.1e400,NaN) takes the poison path (stored raw in:reserved, then dead-lettered) instead of failing the script after the pop.RedisPublisher.publishDelayedscoresfloor(now) + ceil(delay). Limit: no__bq_frame/traceparentin this layout (ADR-0033 §5). -
Legacy
:processingsweep.<q>:processing:deadlinessorted set (member<32-hex token>:<raw>), a Lua sweep before every reserve, and ack/release/DLQ gated on the deadline removal (a late ack is a no-op).retryAfterSeconds(default 90) /retryAfterDisabled()/migrationBatchSizeon the consumer builder. -
Conformance runner for the
redis_layoutandredis_delaysections (pure cases always run; behavioural cases run against a live Redis,BQ_REQUIRE_REDISturns an unreachable Redis into a failure). CI and release run against aredis:7service container. The runner writes aresults.json(ADR-0032) whenBQ_CONFORMANCE_RESULTSis set, which CI validates. -
Zset handler-failure retry backoff (ADR-0034 §9).
RedisConsumer.BuildergainsretryBackoff(double),retryBackoffMultiplier,retryBackoffMax,retryJitter("none"|"full")and a pluggableretryBackoff(IntToDoubleFunction)policy:d(n) = min(max, base * multiplier^(n-1)),nthe 1-based delivery number. Delays are capped atRedisConsumer.MAX_FAILURE_DELAY_SECONDS(30 days);failureDelaySecondsnow rejectsNaN/infinite values.
- Zset layout: a throwing handler is released to
:delayed(attempts + 1) whenfailureDelaySecondsor a retry backoff is configured, with that delay (explicitfailureDelaySecondswins over the backoff). With neither configured the reservation is left alone and the message is redelivered afterretry_after, as in theLEGACYlayout (ADR-0034 §9, .NETLeaveparity). This module has no max-attempts, so a permanently failing message is redelivered indefinitely; configure a backoff and bound retries on the Laravel side. - Depends on
babelqueue-core1.13.0 (was 1.5.0). The envelope is unchanged (schema_version: 1). - In the default
LEGACYlayout a non-conformant envelope (valid JSON that failsEnvelopeCodec.accepts) is now dead-lettered to<queue>.dlq(raw body, nodead_letterblock). Before, it was left in<queue>:processing; with the new deadline sweep it would have been redelivered forever.
- Invalid UTF-8 is poison in both Redis layouts (broker-bindings section 1.1). A stored value (or zset
:reservedmember) that is not valid UTF-8 is dead-lettered on first delivery as the ADR-0035raw/raw_encoding: "base64"wrapper, even when the codec orcjsoncould decode it: it is never handed to a handler and never retried, released or re-published, and the exact stored bytes are used for the settle script, so no byte is rewritten. - The Redis poison dead-letter element is now the ADR-0035 record (raw splice, or the
raw/raw_encodingwrapper) carrying adead_letterblock, instead of the bare element (broker-bindings section 1). Uses a localPoisonRecordcopy (KEEP IN SYNC withbabelqueue-core) until the dependency is bumped. - Poison and non-UTF-8 values are read as bytes. Reserve runs through a
ByteArrayCodecLettuce dispatch, so a stored body that is not valid UTF-8 reaches the ADR-0035 record as the base64 wrapper instead of carrying U+FFFD, and ack / release / dead-letter (including the zsetZREMmember) send the exact stored bytes back. Valid UTF-8 values keep the text path; the publicRedisCommands<String, String>API is unchanged. - A URN with no handler and no
onUnknownUrnhook is now dead-lettered instead of redelivered forever. It used to be reported toonErrorand left reserved, so it came back afterretry_after(or, in the zset layout, after the reservation expired) and looped. It now gets theonErrorcallback (if any) and an annotated record on<queue>.dlq(dead_letter.reason = "unknown_urn",original_queue,attempts, the error andUnknownUrnException; the legacy layout keeps the__bq_frameheader frame sotraceparentsurvives). The reservation is removed by the same gated Lua script that writes the DLQ record (no ack), atomically, in both the legacy:processingand the zset:reservedlayouts (.NET Faz 15 parity). With anonUnknownUrnhook registered the behaviour is unchanged: the hook runs, then the message is acked with no DLQ record. There is no option to disable the dead-lettering. Covered by unit tests in both layouts and live Redis tests (RedisUnknownUrnLiveTest, skipped without Redis, run in CI).
1.1.0 - 2026-06-21
- OpenTelemetry
traceparenttransport wiring (ADR-0028, v0.2). Redis lists have no native per-message metadata channel and the reliable-queue stores the bare body (theLREMack handle is the stored value), so to carry an out-of-band header (e.g. a W3Ctraceparent) beside the frozen envelope the transport now uses a self-contained, transport-owned JSON frame distinct from the wire envelope:{"__bq_frame":1,"headers":…,"body":<raw envelope>}(RedisFrame, mirroring the Go/PHP__bq_frame).RedisPublisher.publishWithHeaders(Envelope, Map<String,String>)is the produce-side seam the optional corecom.babelqueue.otel.HeaderSenderwires to: with usable headers itRPUSHes the frame; with none itRPUSHes the bare envelope byte-for-byte (back-compat).RedisConsumertransparently unframes a reserved value — bare-value back-compat detection via the reserved__bq_framesentinel (a frozen envelope never carries it), theLREMack handle stays the stored value — and surfaces the headers to a new header-awareRedisConsumer.HeaderHandler((envelope, body, headers)), the consume-side seam forTracing.wrapHandler(tracer, handler, Supplier). The existing(envelope, body)BabelHandlerregistration is unchanged. The wire envelope is never touched (GR-1);trace_idis preserved (GR-4);schema_versionstays 1. No new runtime dependency — the frame codec is self-contained and the header seam is a plainMap<String,String>(GR-7).
- Require
com.babelqueue:babelqueue-core 1.5.0(the out-of-band header-carrier seam).
1.0.0 - 2026-06-14
- Initial release. A Redis transport on
babelqueue-core+ the Lettuce client, implementing §1 of the broker-bindings contract (the reliable-queue list pattern):RedisPublisher(produce =RPUSHthe canonical envelope JSON byte-for-byte, no wrapping and no property projection — Redis lists carry no native metadata) andRedisConsumer(reserve =BLMOVE <queue> <queue>:processing LEFT RIGHTso an in-flight message survives a worker crash → URN-routedBabelHandlers → ack viaLREM; at-least-once, a throwing handler leaves the message on the processing list;onError/onUnknownUrnhooks;attemptstaken from the body unchanged since Redis has no native delivery counter). Java 17, JUnit 5, Mockito, JaCoCo ≥90% line coverage (currently 100%); unit tests mock theRedisCommandsseam (no Redis, no network) and capture theRPUSH/BLMOVE/LREMcalls. The envelope is unchanged (schema_version: 1); Redis is purely additive. This is a Java-owned reliable queue; full parity with Laravel's reserved-sorted-set reservation on a shared Redis queue is a separate task (broker-bindings §1.4).