redis-message-queue 2.0.0__tar.gz → 3.0.0__tar.gz
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.
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/PKG-INFO +46 -9
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/README.md +45 -8
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/pyproject.toml +1 -1
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/_abstract_redis_gateway.py +17 -5
- redis_message_queue-3.0.0/redis_message_queue/_config.py +753 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/_queue_key_manager.py +10 -1
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/_redis_gateway.py +289 -215
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/_stored_message.py +10 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/asyncio/_abstract_redis_gateway.py +17 -5
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/asyncio/_redis_gateway.py +290 -223
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/asyncio/redis_message_queue.py +55 -5
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/interrupt_handler/_implementation.py +14 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/redis_message_queue.py +67 -11
- redis_message_queue-2.0.0/redis_message_queue/_config.py +0 -318
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/LICENSE +0 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-2.0.0 → redis_message_queue-3.0.0}/redis_message_queue/interrupt_handler/_interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redis-message-queue
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Python message queuing with Redis and message deduplication
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Elijas
|
|
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
|
|
17
17
|
# redis-message-queue
|
|
18
18
|
|
|
19
|
-
[](https://pypi.org/project/redis-message-queue)
|
|
20
20
|
[](https://pypistats.org/packages/redis-message-queue)
|
|
21
21
|
[](LICENSE)
|
|
22
22
|
[](https://github.com/Elijas/redis-message-queue/issues)
|
|
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
**Lightweight Python message queuing with Redis and built-in publish-side deduplication.** Deduplicate publishes within a TTL window, with optional crash recovery — across any number of producers and consumers.
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
pip install "redis-message-queue>=
|
|
30
|
+
pip install "redis-message-queue>=3.0.0,<4.0.0"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Requires Redis server >= 6.2.
|
|
@@ -78,7 +78,7 @@ while True:
|
|
|
78
78
|
| **Dead-letter queue** | Poison messages that exceed a configurable delivery count are automatically routed to a dead-letter queue instead of being redelivered indefinitely |
|
|
79
79
|
| **Graceful shutdown** | Built-in interrupt handler lets consumers finish current work before stopping |
|
|
80
80
|
| **Lease heartbeats** | Optional background lease renewal keeps long-running handlers from being redelivered prematurely |
|
|
81
|
-
| **Connection retries** | Exponential backoff with jitter for Redis operations (deduplicated publish, ack, lease renewal). Message-claim paths use idempotent Lua claim IDs so retryable errors can recover the original claim safely,
|
|
81
|
+
| **Connection retries** | Exponential backoff with jitter for Redis operations (deduplicated publish, ack, lease renewal). Publish and cleanup paths use replay markers so retryable connection drops preserve the original result within the same call. Message-claim paths use idempotent Lua claim IDs plus persisted claim metadata so retryable errors can recover the original claim safely, either in the same wait call or on the next call from the same gateway instance if the original wait had to give up before Redis became reachable again. Active waits keep their in-flight claim IDs private until they exit, so a concurrent caller on the same gateway instance cannot recover the same claim twice. Timed waits also stay bounded: once the configured wait window expires, the queue only replays persisted state for that same claim attempt and will not claim fresh work after the deadline. If a graceful interrupt arrives during claim recovery, the wait call stops instead of taking fresh work. Non-deduplicated publish is not retried — the exception propagates so the caller can decide whether to retry (accepting potential duplicates) |
|
|
82
82
|
| **Async support** | Drop-in async variant with identical API |
|
|
83
83
|
|
|
84
84
|
All features are optional and can be enabled or disabled as needed.
|
|
@@ -155,6 +155,19 @@ Tradeoffs:
|
|
|
155
155
|
- heartbeats add background renewal work for active messages
|
|
156
156
|
- if a heartbeat fails (network error or stale lease), the heartbeat stops silently; the consumer continues processing but may find at ack time that the message was reclaimed by another consumer
|
|
157
157
|
|
|
158
|
+
Pass `on_heartbeat_failure` to receive a best-effort callback when the heartbeat stops because renewal failed:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
queue = RedisMessageQueue(
|
|
162
|
+
"q", client=client,
|
|
163
|
+
visibility_timeout_seconds=300,
|
|
164
|
+
heartbeat_interval_seconds=60,
|
|
165
|
+
on_heartbeat_failure=lambda: log.warning("heartbeat failed; lease may be stale"),
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The callback is **advisory** — it may fire briefly after a successful `process_message` exit when a final renewal coincided with the success path. Use it for metrics or alerting, not as a correctness signal. For the async queue (`redis_message_queue.asyncio`), the callback may also be `async def`.
|
|
170
|
+
|
|
158
171
|
Without a visibility timeout, messages already moved to `processing` remain there indefinitely after a consumer crash and are not redelivered, even if the crash happened before your handler started running.
|
|
159
172
|
|
|
160
173
|
### Dead-letter queue
|
|
@@ -176,6 +189,7 @@ Notes:
|
|
|
176
189
|
- the delivery count increments when Redis grants the claim/lease, not when your handler begins running. If a process exits after Redis claims a message, that claim still counts toward `max_delivery_count`
|
|
177
190
|
- `max_delivery_count=1` means the message is delivered once; any reclaim routes it to the dead-letter queue
|
|
178
191
|
- without `max_delivery_count`, messages are redelivered indefinitely (existing behavior)
|
|
192
|
+
- dead-lettered messages contain the **raw payload** only — the internal envelope (which carries a per-delivery UUID) is stripped before pushing to the DLQ, consistent with how completed/failed queues store messages. Two identical payloads dead-lettered separately are indistinguishable in the DLQ
|
|
179
193
|
|
|
180
194
|
### Graceful shutdown
|
|
181
195
|
|
|
@@ -196,18 +210,21 @@ while not interrupt.is_interrupted():
|
|
|
196
210
|
> its signals (default: SIGINT, SIGTERM, SIGHUP), but only when those signals are
|
|
197
211
|
> still using Python's default disposition. If another handler is already installed,
|
|
198
212
|
> or if another `GracefulInterruptHandler` already owns the signal, construction raises
|
|
199
|
-
> `ValueError`.
|
|
200
|
-
>
|
|
213
|
+
> `ValueError`. A repeated owned signal falls back to the default behavior
|
|
214
|
+
> (for example, a second Ctrl+C raises `KeyboardInterrupt`). If you need multiple
|
|
215
|
+
> shutdown hooks, use a single handler and fan out in your own code.
|
|
201
216
|
|
|
202
217
|
### Custom gateway
|
|
203
218
|
|
|
204
219
|
```python
|
|
205
220
|
from redis_message_queue._redis_gateway import RedisGateway
|
|
206
221
|
|
|
207
|
-
#
|
|
222
|
+
# Tune retry budget, dedup TTL, or wait interval
|
|
208
223
|
gateway = RedisGateway(
|
|
209
224
|
redis_client=client,
|
|
210
|
-
|
|
225
|
+
retry_budget_seconds=120, # total retry window (set 0 to disable retry)
|
|
226
|
+
retry_max_delay_seconds=5.0, # cap on per-attempt backoff
|
|
227
|
+
retry_initial_delay_seconds=0.01, # first backoff
|
|
211
228
|
message_deduplication_log_ttl_seconds=3600,
|
|
212
229
|
message_wait_interval_seconds=10,
|
|
213
230
|
message_visibility_timeout_seconds=300,
|
|
@@ -215,11 +232,30 @@ gateway = RedisGateway(
|
|
|
215
232
|
queue = RedisMessageQueue("q", gateway=gateway)
|
|
216
233
|
```
|
|
217
234
|
|
|
235
|
+
The retry knobs configure an internal `tenacity` strategy: exponential
|
|
236
|
+
backoff with jitter, retry on transient Redis errors only, capped at
|
|
237
|
+
`retry_budget_seconds`. Setting `retry_budget_seconds=0` disables retry
|
|
238
|
+
entirely (single attempt; exceptions propagate). The library uses
|
|
239
|
+
`retry_budget_seconds` to size the operation-result cache TTL automatically,
|
|
240
|
+
so the previous footgun of an over-long retry budget out-living the cache
|
|
241
|
+
and producing misleading "cleanup was a no-op" warnings is now structurally
|
|
242
|
+
impossible.
|
|
243
|
+
|
|
244
|
+
To plug in a different retry library (`backoff`, `asyncstdlib.retry`, or your
|
|
245
|
+
own logic) or fundamentally different semantics, subclass
|
|
246
|
+
`AbstractRedisGateway` from `redis_message_queue._abstract_redis_gateway`
|
|
247
|
+
(or `redis_message_queue.asyncio._abstract_redis_gateway`) and override the
|
|
248
|
+
operation methods directly.
|
|
249
|
+
|
|
218
250
|
If your custom gateway uses visibility timeouts, it must expose a public
|
|
219
251
|
`message_visibility_timeout_seconds` value and return `ClaimedMessage` from
|
|
220
252
|
`wait_for_message_and_move()`. The queue now fails closed if a lease-capable
|
|
221
253
|
gateway returns plain `str`/`bytes`, because cleanup without a lease token can
|
|
222
254
|
ack a message that has already been reclaimed by another consumer.
|
|
255
|
+
If a lease-capable custom gateway omits `message_visibility_timeout_seconds`,
|
|
256
|
+
the queue cannot detect that lease semantics are in play and will treat the
|
|
257
|
+
gateway as a non-lease gateway. In that misconfigured state, lease-token safety
|
|
258
|
+
checks and heartbeat validation are bypassed.
|
|
223
259
|
|
|
224
260
|
When using a custom gateway with dead-letter queue support, configure `max_delivery_count`
|
|
225
261
|
and `dead_letter_queue` directly on the gateway — do **not** pass `max_delivery_count` to
|
|
@@ -261,9 +297,10 @@ await client.aclose()
|
|
|
261
297
|
|
|
262
298
|
- **No metrics or observability hooks.** The library logs warnings (stale leases, heartbeat failures, transient errors) via Python's `logging` module but does not expose callbacks, event hooks, or metric counters. To monitor queue health, inspect the underlying Redis keys directly or parse log output.
|
|
263
299
|
- **Timed waits use polling claim loops.** To make claims recoverable after ambiguous connection drops, `wait_for_message_and_move()` uses idempotent Lua claim polling instead of raw blocking list-move commands. This adds a small polling cadence during timed waits.
|
|
264
|
-
- **
|
|
300
|
+
- **Redis Lua is atomic, not rollback-transactional.** The built-in scripts now preflight queue key types and fail closed on `WRONGTYPE` before mutating queue state, but Redis does not undo earlier writes if a later script command fails for another reason (for example `OOM` under severe memory pressure).
|
|
265
301
|
- **Batch reclaim limit of 100.** The visibility-timeout reclaim Lua script processes at most 100 expired messages per consumer poll. Under extreme backlog this may delay recovery, but prevents any single poll from blocking Redis.
|
|
266
302
|
- **Redis Cluster requires hash tags.** The built-in queue uses multiple Redis keys per operation. Wrap the queue name in hash tags (for example `{myqueue}`) so every generated key lands in the same slot. When you pass a Redis Cluster client to the built-in queue/gateway path, incompatible names are rejected early.
|
|
303
|
+
- **Client-side `Retry` can duplicate non-deduplicated publishes.** If you construct your `redis.Redis` client with `retry=Retry(...)`, redis-py retries `ConnectionError` / `TimeoutError` at the connection layer — *below* this library. Idempotent operations (deduplicated `publish()`, lease-scoped cleanup) are safe because their Lua scripts replay the original result. `add_message()` (used by `publish()` when `deduplication=False`) is a bare `LPUSH`: this library deliberately does not retry it, but a client-level `Retry` will, and if the server executed the command before the response was lost the message is enqueued twice. Leave `retry=None` (the default) if you need strict at-most-once semantics for non-deduplicated publishes, or accept the duplication risk.
|
|
267
304
|
|
|
268
305
|
For a full analysis, see [docs/production-readiness.md](docs/production-readiness.md).
|
|
269
306
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# redis-message-queue
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/redis-message-queue)
|
|
4
4
|
[](https://pypistats.org/packages/redis-message-queue)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/Elijas/redis-message-queue/issues)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**Lightweight Python message queuing with Redis and built-in publish-side deduplication.** Deduplicate publishes within a TTL window, with optional crash recovery — across any number of producers and consumers.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
pip install "redis-message-queue>=
|
|
14
|
+
pip install "redis-message-queue>=3.0.0,<4.0.0"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Requires Redis server >= 6.2.
|
|
@@ -62,7 +62,7 @@ while True:
|
|
|
62
62
|
| **Dead-letter queue** | Poison messages that exceed a configurable delivery count are automatically routed to a dead-letter queue instead of being redelivered indefinitely |
|
|
63
63
|
| **Graceful shutdown** | Built-in interrupt handler lets consumers finish current work before stopping |
|
|
64
64
|
| **Lease heartbeats** | Optional background lease renewal keeps long-running handlers from being redelivered prematurely |
|
|
65
|
-
| **Connection retries** | Exponential backoff with jitter for Redis operations (deduplicated publish, ack, lease renewal). Message-claim paths use idempotent Lua claim IDs so retryable errors can recover the original claim safely,
|
|
65
|
+
| **Connection retries** | Exponential backoff with jitter for Redis operations (deduplicated publish, ack, lease renewal). Publish and cleanup paths use replay markers so retryable connection drops preserve the original result within the same call. Message-claim paths use idempotent Lua claim IDs plus persisted claim metadata so retryable errors can recover the original claim safely, either in the same wait call or on the next call from the same gateway instance if the original wait had to give up before Redis became reachable again. Active waits keep their in-flight claim IDs private until they exit, so a concurrent caller on the same gateway instance cannot recover the same claim twice. Timed waits also stay bounded: once the configured wait window expires, the queue only replays persisted state for that same claim attempt and will not claim fresh work after the deadline. If a graceful interrupt arrives during claim recovery, the wait call stops instead of taking fresh work. Non-deduplicated publish is not retried — the exception propagates so the caller can decide whether to retry (accepting potential duplicates) |
|
|
66
66
|
| **Async support** | Drop-in async variant with identical API |
|
|
67
67
|
|
|
68
68
|
All features are optional and can be enabled or disabled as needed.
|
|
@@ -139,6 +139,19 @@ Tradeoffs:
|
|
|
139
139
|
- heartbeats add background renewal work for active messages
|
|
140
140
|
- if a heartbeat fails (network error or stale lease), the heartbeat stops silently; the consumer continues processing but may find at ack time that the message was reclaimed by another consumer
|
|
141
141
|
|
|
142
|
+
Pass `on_heartbeat_failure` to receive a best-effort callback when the heartbeat stops because renewal failed:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
queue = RedisMessageQueue(
|
|
146
|
+
"q", client=client,
|
|
147
|
+
visibility_timeout_seconds=300,
|
|
148
|
+
heartbeat_interval_seconds=60,
|
|
149
|
+
on_heartbeat_failure=lambda: log.warning("heartbeat failed; lease may be stale"),
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The callback is **advisory** — it may fire briefly after a successful `process_message` exit when a final renewal coincided with the success path. Use it for metrics or alerting, not as a correctness signal. For the async queue (`redis_message_queue.asyncio`), the callback may also be `async def`.
|
|
154
|
+
|
|
142
155
|
Without a visibility timeout, messages already moved to `processing` remain there indefinitely after a consumer crash and are not redelivered, even if the crash happened before your handler started running.
|
|
143
156
|
|
|
144
157
|
### Dead-letter queue
|
|
@@ -160,6 +173,7 @@ Notes:
|
|
|
160
173
|
- the delivery count increments when Redis grants the claim/lease, not when your handler begins running. If a process exits after Redis claims a message, that claim still counts toward `max_delivery_count`
|
|
161
174
|
- `max_delivery_count=1` means the message is delivered once; any reclaim routes it to the dead-letter queue
|
|
162
175
|
- without `max_delivery_count`, messages are redelivered indefinitely (existing behavior)
|
|
176
|
+
- dead-lettered messages contain the **raw payload** only — the internal envelope (which carries a per-delivery UUID) is stripped before pushing to the DLQ, consistent with how completed/failed queues store messages. Two identical payloads dead-lettered separately are indistinguishable in the DLQ
|
|
163
177
|
|
|
164
178
|
### Graceful shutdown
|
|
165
179
|
|
|
@@ -180,18 +194,21 @@ while not interrupt.is_interrupted():
|
|
|
180
194
|
> its signals (default: SIGINT, SIGTERM, SIGHUP), but only when those signals are
|
|
181
195
|
> still using Python's default disposition. If another handler is already installed,
|
|
182
196
|
> or if another `GracefulInterruptHandler` already owns the signal, construction raises
|
|
183
|
-
> `ValueError`.
|
|
184
|
-
>
|
|
197
|
+
> `ValueError`. A repeated owned signal falls back to the default behavior
|
|
198
|
+
> (for example, a second Ctrl+C raises `KeyboardInterrupt`). If you need multiple
|
|
199
|
+
> shutdown hooks, use a single handler and fan out in your own code.
|
|
185
200
|
|
|
186
201
|
### Custom gateway
|
|
187
202
|
|
|
188
203
|
```python
|
|
189
204
|
from redis_message_queue._redis_gateway import RedisGateway
|
|
190
205
|
|
|
191
|
-
#
|
|
206
|
+
# Tune retry budget, dedup TTL, or wait interval
|
|
192
207
|
gateway = RedisGateway(
|
|
193
208
|
redis_client=client,
|
|
194
|
-
|
|
209
|
+
retry_budget_seconds=120, # total retry window (set 0 to disable retry)
|
|
210
|
+
retry_max_delay_seconds=5.0, # cap on per-attempt backoff
|
|
211
|
+
retry_initial_delay_seconds=0.01, # first backoff
|
|
195
212
|
message_deduplication_log_ttl_seconds=3600,
|
|
196
213
|
message_wait_interval_seconds=10,
|
|
197
214
|
message_visibility_timeout_seconds=300,
|
|
@@ -199,11 +216,30 @@ gateway = RedisGateway(
|
|
|
199
216
|
queue = RedisMessageQueue("q", gateway=gateway)
|
|
200
217
|
```
|
|
201
218
|
|
|
219
|
+
The retry knobs configure an internal `tenacity` strategy: exponential
|
|
220
|
+
backoff with jitter, retry on transient Redis errors only, capped at
|
|
221
|
+
`retry_budget_seconds`. Setting `retry_budget_seconds=0` disables retry
|
|
222
|
+
entirely (single attempt; exceptions propagate). The library uses
|
|
223
|
+
`retry_budget_seconds` to size the operation-result cache TTL automatically,
|
|
224
|
+
so the previous footgun of an over-long retry budget out-living the cache
|
|
225
|
+
and producing misleading "cleanup was a no-op" warnings is now structurally
|
|
226
|
+
impossible.
|
|
227
|
+
|
|
228
|
+
To plug in a different retry library (`backoff`, `asyncstdlib.retry`, or your
|
|
229
|
+
own logic) or fundamentally different semantics, subclass
|
|
230
|
+
`AbstractRedisGateway` from `redis_message_queue._abstract_redis_gateway`
|
|
231
|
+
(or `redis_message_queue.asyncio._abstract_redis_gateway`) and override the
|
|
232
|
+
operation methods directly.
|
|
233
|
+
|
|
202
234
|
If your custom gateway uses visibility timeouts, it must expose a public
|
|
203
235
|
`message_visibility_timeout_seconds` value and return `ClaimedMessage` from
|
|
204
236
|
`wait_for_message_and_move()`. The queue now fails closed if a lease-capable
|
|
205
237
|
gateway returns plain `str`/`bytes`, because cleanup without a lease token can
|
|
206
238
|
ack a message that has already been reclaimed by another consumer.
|
|
239
|
+
If a lease-capable custom gateway omits `message_visibility_timeout_seconds`,
|
|
240
|
+
the queue cannot detect that lease semantics are in play and will treat the
|
|
241
|
+
gateway as a non-lease gateway. In that misconfigured state, lease-token safety
|
|
242
|
+
checks and heartbeat validation are bypassed.
|
|
207
243
|
|
|
208
244
|
When using a custom gateway with dead-letter queue support, configure `max_delivery_count`
|
|
209
245
|
and `dead_letter_queue` directly on the gateway — do **not** pass `max_delivery_count` to
|
|
@@ -245,9 +281,10 @@ await client.aclose()
|
|
|
245
281
|
|
|
246
282
|
- **No metrics or observability hooks.** The library logs warnings (stale leases, heartbeat failures, transient errors) via Python's `logging` module but does not expose callbacks, event hooks, or metric counters. To monitor queue health, inspect the underlying Redis keys directly or parse log output.
|
|
247
283
|
- **Timed waits use polling claim loops.** To make claims recoverable after ambiguous connection drops, `wait_for_message_and_move()` uses idempotent Lua claim polling instead of raw blocking list-move commands. This adds a small polling cadence during timed waits.
|
|
248
|
-
- **
|
|
284
|
+
- **Redis Lua is atomic, not rollback-transactional.** The built-in scripts now preflight queue key types and fail closed on `WRONGTYPE` before mutating queue state, but Redis does not undo earlier writes if a later script command fails for another reason (for example `OOM` under severe memory pressure).
|
|
249
285
|
- **Batch reclaim limit of 100.** The visibility-timeout reclaim Lua script processes at most 100 expired messages per consumer poll. Under extreme backlog this may delay recovery, but prevents any single poll from blocking Redis.
|
|
250
286
|
- **Redis Cluster requires hash tags.** The built-in queue uses multiple Redis keys per operation. Wrap the queue name in hash tags (for example `{myqueue}`) so every generated key lands in the same slot. When you pass a Redis Cluster client to the built-in queue/gateway path, incompatible names are rejected early.
|
|
287
|
+
- **Client-side `Retry` can duplicate non-deduplicated publishes.** If you construct your `redis.Redis` client with `retry=Retry(...)`, redis-py retries `ConnectionError` / `TimeoutError` at the connection layer — *below* this library. Idempotent operations (deduplicated `publish()`, lease-scoped cleanup) are safe because their Lua scripts replay the original result. `add_message()` (used by `publish()` when `deduplication=False`) is a bare `LPUSH`: this library deliberately does not retry it, but a client-level `Retry` will, and if the server executed the command before the response was lost the message is enqueued twice. Leave `retry=None` (the default) if you need strict at-most-once semantics for non-deduplicated publishes, or accept the duplication risk.
|
|
251
288
|
|
|
252
289
|
For a full analysis, see [docs/production-readiness.md](docs/production-readiness.md).
|
|
253
290
|
|
|
@@ -15,6 +15,9 @@ class AbstractRedisGateway(ABC):
|
|
|
15
15
|
a ``message_visibility_timeout_seconds`` property (int or None). This is not
|
|
16
16
|
abstract because it is configuration rather than protocol, but it is required
|
|
17
17
|
when the queue is configured with ``heartbeat_interval_seconds``.
|
|
18
|
+
Lease-capable custom gateways should always expose this property; otherwise
|
|
19
|
+
the queue cannot enforce lease-specific fail-closed checks and will treat the
|
|
20
|
+
gateway as a non-lease implementation.
|
|
18
21
|
|
|
19
22
|
Concurrency
|
|
20
23
|
-----------
|
|
@@ -57,9 +60,13 @@ class AbstractRedisGateway(ABC):
|
|
|
57
60
|
|
|
58
61
|
When ``lease_token`` is provided, the implementation MUST validate that
|
|
59
62
|
the token matches the current lease holder before moving. If the token
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
no longer matches the current lease holder (i.e. another consumer has
|
|
64
|
+
reclaimed the message), the method MUST return False and leave the
|
|
65
|
+
message in ``from_queue``. Note: the built-in gateway intentionally
|
|
66
|
+
does NOT reject completions whose wall-clock deadline has passed but
|
|
67
|
+
where no other consumer has reclaimed the message — that path keeps
|
|
68
|
+
at-least-once semantics from producing spurious double-processing.
|
|
69
|
+
Ignoring ``lease_token`` entirely silently breaks mutual exclusion.
|
|
63
70
|
|
|
64
71
|
Returns True if the message was moved, False otherwise.
|
|
65
72
|
"""
|
|
@@ -75,8 +82,13 @@ class AbstractRedisGateway(ABC):
|
|
|
75
82
|
|
|
76
83
|
When ``lease_token`` is provided, the implementation MUST validate that
|
|
77
84
|
the token matches the current lease holder before removing. If the token
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
no longer matches the current lease holder (i.e. another consumer has
|
|
86
|
+
reclaimed the message), the method MUST return False. Note: the
|
|
87
|
+
built-in gateway intentionally does NOT reject completions whose
|
|
88
|
+
wall-clock deadline has passed but where no other consumer has
|
|
89
|
+
reclaimed the message — that path keeps at-least-once semantics from
|
|
90
|
+
producing spurious double-processing. Ignoring ``lease_token``
|
|
91
|
+
entirely silently breaks mutual exclusion.
|
|
80
92
|
|
|
81
93
|
Returns True if the message was removed, False otherwise.
|
|
82
94
|
"""
|