@babelqueue/redis 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -15
- package/dist/index.cjs +686 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +276 -65
- package/dist/index.d.ts +276 -65
- package/dist/index.js +680 -57
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -8,11 +8,14 @@ the same wire contract as the PHP, Python and Go SDKs. Implements
|
|
|
8
8
|
Redis lists carry **no native metadata** — the list element **is** the canonical envelope JSON,
|
|
9
9
|
byte-for-byte, with **no wrapping** (unlike `@babelqueue/bullmq`, which uses BullMQ's own job
|
|
10
10
|
layout). Produce is `RPUSH`; consume reserves the head into a `<queue>:processing` list
|
|
11
|
-
(`
|
|
11
|
+
(`LMOVE`, so an in-flight message survives a crash), routes by URN, and acknowledges it on success.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Two layouts are available ([ADR-0033](https://babelqueue.com/docs/spec/1.x/broker-bindings#redis)):
|
|
14
|
+
|
|
15
|
+
- **`legacy`** (default) — the Node-owned `:processing` reliable queue, now with deadline recovery.
|
|
16
|
+
- **`zset`** — the Laravel-compatible `queues:<q>` / `:delayed` / `:reserved` / `:notify` layout, so a
|
|
17
|
+
Node worker can share a queue with a Laravel producer or worker, with native delays and
|
|
18
|
+
`retry_after` recovery.
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
@@ -21,7 +24,9 @@ npm install @babelqueue/redis ioredis
|
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
`ioredis` is an optional peer — you provide the client; an `ioredis` instance satisfies the
|
|
24
|
-
adapter structurally.
|
|
27
|
+
adapter structurally. Consuming also needs `evalBuffer` (Lua eval returning raw `Buffer`s, so a
|
|
28
|
+
non-UTF-8 body can be detected as poison); without it `reserve()` throws. A non-`ioredis` client
|
|
29
|
+
must provide it.
|
|
25
30
|
|
|
26
31
|
## Produce
|
|
27
32
|
|
|
@@ -54,10 +59,51 @@ const consumer = new RedisConsumer(client, "orders", handlers, {
|
|
|
54
59
|
await consumer.run(() => true); // reserve → process → LREM, until you stop it
|
|
55
60
|
```
|
|
56
61
|
|
|
57
|
-
A successful handler
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`
|
|
62
|
+
A successful handler acknowledges the reservation. A throwing handler gives the envelope back with
|
|
63
|
+
`attempts + 1` (at-least-once) up to `maxTries`, then dead-letters to `<queue>.dlq` with a
|
|
64
|
+
`dead_letter` block. Unknown-URN strategy is one of `fail` / `delete` / `release` / `dead_letter`; the default `fail` reports the error and retries with `attempts + 1` up to `maxTries`, then dead-letters (`reason: "unknown_urn"`).
|
|
65
|
+
`poll()` and `handle(reservation)` are exposed for testing.
|
|
66
|
+
|
|
67
|
+
`blockTimeout` (seconds, default 5) is how long one `poll()` waits: a sleep-poll (100 ms, doubling,
|
|
68
|
+
capped at 1 s) in the legacy layout, `BLPOP` on `:notify` (at most 1 s per slice, migrating between
|
|
69
|
+
slices) in the zset layout. `0` or less means a single non-blocking attempt.
|
|
70
|
+
|
|
71
|
+
## Zset layout, delays and recovery
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const publisher = RedisPublisher.create(client, "orders", { layout: "zset" });
|
|
75
|
+
await publisher.publish("urn:babel:orders:created", { order_id: 1 }, { delay: 30 }); // ZADD :delayed
|
|
76
|
+
|
|
77
|
+
const consumer = new RedisConsumer(client, "orders", handlers, {
|
|
78
|
+
layout: "zset",
|
|
79
|
+
retryAfter: 90, // seconds before an unacked reservation is recovered; null disables it
|
|
80
|
+
migrationBatchSize: -1, // max items moved per migrate (-1 = unlimited)
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- An immediate publish is `RPUSH queues:<q>` plus one `RPUSH queues:<q>:notify 1`; a delayed one is
|
|
85
|
+
`ZADD queues:<q>:delayed floor(now)+ceil(delay)` (no notify token).
|
|
86
|
+
- Every pop first migrates due `:delayed` items, then expired `:reserved` items (skipped when
|
|
87
|
+
`retryAfter` is `null`), to the end of the ready list. A recovery counts as an attempt.
|
|
88
|
+
- Reserve is one Lua script: it increments `attempts` in the stored reservation, while the handler
|
|
89
|
+
still sees the body you published. A body that is not JSON, or whose `attempts` is not a number
|
|
90
|
+
(poison), is dead-lettered to `<queue>.dlq` and removed from `:reserved`.
|
|
91
|
+
- A release is one Lua script (`ZREM :reserved` + `ZADD :delayed`) that raises `attempts` by one and
|
|
92
|
+
preserves every other key. Integers above 2^53 lose precision in that rewrite (`JSON.parse`).
|
|
93
|
+
- Cluster hash tags are the operator's responsibility (pass them in the queue name).
|
|
94
|
+
- The handler timeout SHOULD be shorter than `retryAfter`, otherwise a slow handler is redelivered
|
|
95
|
+
while still running. A negative `retryAfter` is rejected with a `BabelQueueError`.
|
|
96
|
+
- **No headers in the zset layout.** The Laravel wire has no place for the `__bq_frame` wrapper, so
|
|
97
|
+
`headers` (including `traceparent`) are dropped on publish and the handler's third argument is
|
|
98
|
+
always `{}` (A3). Use the legacy layout when trace-context propagation matters.
|
|
99
|
+
|
|
100
|
+
## Legacy recovery
|
|
101
|
+
|
|
102
|
+
Each reservation also writes a member `<32 hex token>:<raw>` into the `<queue>:processing:deadlines`
|
|
103
|
+
sorted set (score `floor(now)+retryAfter`; `+inf` when `retryAfter` is `null`). A sweep before every
|
|
104
|
+
reserve moves expired entries back to the ready list byte-for-byte (no `attempts` change), so a
|
|
105
|
+
crashed consumer's message is not lost. An ack that arrives after the sweep touches nothing and logs
|
|
106
|
+
a "reservation expired" warning. Tokens come from `crypto.randomBytes`.
|
|
61
107
|
|
|
62
108
|
## Contract mapping (§1)
|
|
63
109
|
|
|
@@ -65,20 +111,26 @@ the envelope with `attempts + 1` (at-least-once) up to `maxTries`, then dead-let
|
|
|
65
111
|
| :--- | :--- |
|
|
66
112
|
| body | the list element — the canonical envelope JSON, **verbatim** (no wrapping) |
|
|
67
113
|
| produce | `RPUSH <queue> <envelope>` |
|
|
68
|
-
| reserve | `
|
|
69
|
-
| ack | `LREM <queue>:processing 1 <element
|
|
70
|
-
| retry |
|
|
114
|
+
| reserve | legacy: `LMOVE <queue> <queue>:processing LEFT RIGHT` + deadline; zset: the Laravel reserve script |
|
|
115
|
+
| ack | legacy: `ZREM` deadline, then `LREM <queue>:processing 1 <element>`; zset: `ZREM :reserved` |
|
|
116
|
+
| retry | give back with `attempts + 1` (the body owns the count) |
|
|
71
117
|
| dead-letter | `RPUSH <queue>.dlq` + `dead_letter` block |
|
|
72
118
|
|
|
73
|
-
The envelope is unchanged (`schema_version` stays `1`). The
|
|
74
|
-
|
|
119
|
+
The envelope is unchanged (`schema_version` stays `1`). The unit suite uses a fake client; the Lua
|
|
120
|
+
scripts and the conformance `redis_layout` cases are exercised against a real Redis
|
|
121
|
+
(`BABELQUEUE_REDIS_URL`, default `redis://127.0.0.1:6379`). With `BQ_REQUIRE_REDIS` set an
|
|
122
|
+
unreachable Redis fails the run; without it those tests are skipped.
|
|
123
|
+
|
|
124
|
+
When `BQ_CONFORMANCE_RESULTS` is set, the conformance run writes `results.redis.json` beside the
|
|
125
|
+
given path (the package slug is inserted so several packages sharing one value do not overwrite each
|
|
126
|
+
other).
|
|
75
127
|
|
|
76
128
|
## OpenTelemetry tracing (ADR-0028)
|
|
77
129
|
|
|
78
130
|
Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
|
|
79
131
|
[`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
|
|
80
132
|
`@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
|
|
81
|
-
a transport-owned `__bq_frame` JSON frame (Redis lists have no native metadata channel; the `LREM` ack handle *is* the stored value). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
|
|
133
|
+
a transport-owned `__bq_frame` JSON frame in the **legacy** layout (Redis lists have no native metadata channel; the `LREM` ack handle *is* the stored value). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
|
|
82
134
|
so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
|
|
83
135
|
|
|
84
136
|
A bare (pre-0028 / cross-version) value consumes with empty headers, and a header-less publish stays byte-identical.
|