Redis transport
Last updated:
com.babelqueue:babelqueue-redis is a Redis transport on the Java core, built on the
Lettuce client. It sends the
canonical envelope as a Redis list element with the
§1 reliable-queue pattern, and consumes by routing each
message to a handler by URN — so a message it produces is consumed by any other BabelQueue SDK,
and vice-versa.
Redis lists carry no native metadata, so — unlike the SQS, RabbitMQ or Kafka bindings — there
is no property projection. By default the list element is the canonical envelope JSON,
byte-for-byte, with no wrapping and no added fields; routing and tracing read the body directly.
When headers are published (publishWithHeaders, e.g. a W3C traceparent per
ADR-0028), the element is instead a __bq_frame
wrapper {"__bq_frame":1,"headers":…,"body":<envelope>} that every consumer unwraps; the envelope
inside is still unchanged.
Install
Maven:
<dependency>
<groupId>com.babelqueue</groupId>
<artifactId>babelqueue-redis</artifactId>
<version>1.1.0</version>
</dependency>
Requirements: Java 17+. It pulls babelqueue-core and io.lettuce:lettuce-core transitively.
The consumer reserves with a Lua script that issues LMOVE, so it needs Redis 6.2+. On Redis Cluster, use
hash-tagged queue names; see Redis compatibility.
Produce
import com.babelqueue.redis.RedisPublisher;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.sync.RedisCommands;
import java.util.Map;
RedisClient client = RedisClient.create("redis://localhost:6379");
RedisCommands<String, String> redis = client.connect().sync(); // the command seam
String id = RedisPublisher.create(redis, "orders")
.publish("urn:babel:orders:created", Map.of("order_id", 1042L));
publish(urn, data) returns the message meta.id; an overload adds a traceId to continue a
trace. Produce is RPUSH <queue> <envelope>.
Consume
import com.babelqueue.redis.RedisConsumer;
RedisConsumer consumer = RedisConsumer.builder(redis, "orders")
.handler("urn:babel:orders:created", (env, body) -> {
// env.data(), env.traceId(), env.attempts() ...
})
.onError((err, env, body) -> log.warn("bad message", err))
.build();
consumer.run(); // sleep-polls until the thread is interrupted
A reserve atomically moves the head of the queue onto a per-queue <queue>:processing list
(a Lua script that issues LMOVE <queue> <queue>:processing LEFT RIGHT, polling with a 100 ms to 1 s
backoff because Lua cannot block), so an in-flight message survives a crash; a successful handler
LREMs it. Retry is at-least-once: a throwing handler leaves the element on the processing
list until the recovery sweep requeues it after retry_after. The poll loop never stops on a bad message —
observe via onError / onUnknownUrn. An unknown URN is passed to onUnknownUrn (then acked)
if one is set; otherwise it is reported to onError and dead-lettered to <queue>.dlq with
dead_letter.reason: "unknown_urn" (a fixed dead_letter, a documented deviation from the generic
fail default). The DLQ write is unconditional: there is no DLQ opt-in switch, so it never degrades to delete.
The command seam is Lettuce’s RedisCommands<String, String> interface, so the publisher and
consumer unit-test against a mock with no Redis and no network.
Recovery and the zset layout
The default (RedisLayout.LEGACY) layout writes a deadline member to
<queue>:processing:deadlines on every reserve and sweeps expired ones back to the ready list
before the next reserve. The window is retry_after, 90 seconds by default:
retryAfterSeconds(long) sets it and retryAfterDisabled() is retry_after: null (see the
+inf note). Keep the
handler timeout shorter than retry_after. A legacy recovery does not increment attempts, and
the legacy layout has no delayed set: a delayed release is rejected there.
For Laravel’s sorted-set layout, set .layout(RedisLayout.ZSET) on both the publisher and the
consumer builder. It adds :delayed, :reserved and :notify, delayed publishes, and recovery that
counts as an attempt. See Redis in the binding spec.
Contract mapping (§1)
| Envelope | Redis |
|---|---|
| body | the list element — the canonical envelope JSON, byte-for-byte, no wrapping (a __bq_frame wrapper only when publishWithHeaders carries headers) |
job (URN) |
read from the body and routed consumer-side (Redis lists carry no native metadata) |
| produce | RPUSH <queue> <envelope> |
| reserve | Lua script: LMOVE <queue> <queue>:processing LEFT RIGHT plus a deadline member (head → tail; crash-safe in-flight) |
| ack | LREM <queue>:processing 1 <envelope> |
attempts |
taken from the body unchanged (Redis has no native delivery counter) |
This is a Java-owned reliable queue, mirroring the Go runtime’s reliable-queue mechanism —
pointed at a queue this SDK owns end-to-end it is a complete, crash-safe transport. Full parity with
Laravel’s reserved-sorted-set reservation on a shared PHP+Java Redis queue is a separate task (see
broker-bindings §1.4). The envelope is unchanged (schema_version stays 1); Redis is purely
additive.