redis-message-queue 3.1.1__tar.gz → 3.1.2__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-3.1.1 → redis_message_queue-3.1.2}/PKG-INFO +18 -2
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/README.md +17 -1
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/pyproject.toml +1 -1
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/__init__.py +2 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_abstract_redis_gateway.py +4 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_config.py +31 -16
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_queue_key_manager.py +2 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_redis_gateway.py +3 -3
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/asyncio/__init__.py +2 -1
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/asyncio/_abstract_redis_gateway.py +4 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/asyncio/_redis_gateway.py +3 -3
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/asyncio/redis_message_queue.py +1 -1
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/redis_message_queue.py +2 -2
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/LICENSE +0 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/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.1.
|
|
3
|
+
Version: 3.1.2
|
|
4
4
|
Summary: Python message queuing with Redis and message deduplication
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# redis-message-queue
|
|
28
28
|
|
|
29
|
-
[](https://pypi.org/project/redis-message-queue)
|
|
30
30
|
[](https://pypistats.org/packages/redis-message-queue)
|
|
31
31
|
[](LICENSE)
|
|
32
32
|
[](https://github.com/Elijas/redis-message-queue/issues)
|
|
@@ -310,12 +310,28 @@ await client.aclose()
|
|
|
310
310
|
- **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).
|
|
311
311
|
- **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.
|
|
312
312
|
- **Claim-attempt loop limit of 100 per poll.** The VT claim Lua script attempts at most 100 LMOVE+delivery-count checks per invocation. Under pathological conditions (>100 consecutive poison messages in pending), a single poll returns no message even though non-poison messages exist deeper in the queue. Subsequent polls drain the poison batch 100 at a time.
|
|
313
|
+
- **Default dedup key is the full message.** Without a custom `get_deduplication_key`, the entire serialized message becomes a Redis key name for dedup tracking. For large messages (>1KB), provide a custom key function to avoid excessive Redis memory usage.
|
|
313
314
|
- **Cluster detection uses `isinstance(client, RedisCluster)`.** Wrapped or instrumented cluster clients that delegate without inheriting will bypass hash-tag validation. Custom gateways should set `is_redis_cluster = True` explicitly.
|
|
314
315
|
- **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.
|
|
316
|
+
- **Non-ASCII payloads use ~2x storage.** The default `ensure_ascii=True` in JSON serialization encodes non-ASCII characters as `\uXXXX` escape sequences. This is a deliberate compatibility choice.
|
|
315
317
|
- **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. More broadly, any non-idempotent `LPUSH` path is vulnerable if the connection drops after server execution but before the client receives the response; all other built-in operations (deduplicated publish, lease-scoped ack/move, lease renewal) use replay markers and are safe under client-level `Retry`.
|
|
316
318
|
|
|
317
319
|
For a full analysis, see [docs/production-readiness.md](docs/production-readiness.md).
|
|
318
320
|
|
|
321
|
+
## Upgrading
|
|
322
|
+
|
|
323
|
+
### Configuration changes on live queues
|
|
324
|
+
|
|
325
|
+
> **Warning:** These changes are destructive on live queues. Drain the queue completely before applying them.
|
|
326
|
+
|
|
327
|
+
- **Do not change `key_separator` on a live queue.** All existing Redis keys become invisible to the new key scheme. Drain the queue completely before changing separators.
|
|
328
|
+
- **Do not switch from no-VT to VT with messages in processing.** Messages claimed by non-VT consumers have no lease deadline entries. VT-enabled consumers cannot reclaim them. Drain the processing queue first.
|
|
329
|
+
- **Reducing `max_delivery_count` retroactively DLQs messages.** The delivery count hash persists across restarts. Messages whose accumulated count exceeds the new limit are immediately dead-lettered on next claim.
|
|
330
|
+
|
|
331
|
+
### v2 to v3 migration
|
|
332
|
+
|
|
333
|
+
v3.0.0 replaced the `retry_strategy: Callable` constructor parameter with `retry_budget_seconds`, `retry_max_delay_seconds`, and `retry_initial_delay_seconds`. Users with custom retry strategies should subclass `AbstractRedisGateway` instead (see [Custom gateway](#custom-gateway)).
|
|
334
|
+
|
|
319
335
|
## Running locally
|
|
320
336
|
|
|
321
337
|
You'll need a Redis server:
|
|
@@ -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)
|
|
@@ -284,12 +284,28 @@ await client.aclose()
|
|
|
284
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).
|
|
285
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.
|
|
286
286
|
- **Claim-attempt loop limit of 100 per poll.** The VT claim Lua script attempts at most 100 LMOVE+delivery-count checks per invocation. Under pathological conditions (>100 consecutive poison messages in pending), a single poll returns no message even though non-poison messages exist deeper in the queue. Subsequent polls drain the poison batch 100 at a time.
|
|
287
|
+
- **Default dedup key is the full message.** Without a custom `get_deduplication_key`, the entire serialized message becomes a Redis key name for dedup tracking. For large messages (>1KB), provide a custom key function to avoid excessive Redis memory usage.
|
|
287
288
|
- **Cluster detection uses `isinstance(client, RedisCluster)`.** Wrapped or instrumented cluster clients that delegate without inheriting will bypass hash-tag validation. Custom gateways should set `is_redis_cluster = True` explicitly.
|
|
288
289
|
- **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.
|
|
290
|
+
- **Non-ASCII payloads use ~2x storage.** The default `ensure_ascii=True` in JSON serialization encodes non-ASCII characters as `\uXXXX` escape sequences. This is a deliberate compatibility choice.
|
|
289
291
|
- **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. More broadly, any non-idempotent `LPUSH` path is vulnerable if the connection drops after server execution but before the client receives the response; all other built-in operations (deduplicated publish, lease-scoped ack/move, lease renewal) use replay markers and are safe under client-level `Retry`.
|
|
290
292
|
|
|
291
293
|
For a full analysis, see [docs/production-readiness.md](docs/production-readiness.md).
|
|
292
294
|
|
|
295
|
+
## Upgrading
|
|
296
|
+
|
|
297
|
+
### Configuration changes on live queues
|
|
298
|
+
|
|
299
|
+
> **Warning:** These changes are destructive on live queues. Drain the queue completely before applying them.
|
|
300
|
+
|
|
301
|
+
- **Do not change `key_separator` on a live queue.** All existing Redis keys become invisible to the new key scheme. Drain the queue completely before changing separators.
|
|
302
|
+
- **Do not switch from no-VT to VT with messages in processing.** Messages claimed by non-VT consumers have no lease deadline entries. VT-enabled consumers cannot reclaim them. Drain the processing queue first.
|
|
303
|
+
- **Reducing `max_delivery_count` retroactively DLQs messages.** The delivery count hash persists across restarts. Messages whose accumulated count exceeds the new limit are immediately dead-lettered on next claim.
|
|
304
|
+
|
|
305
|
+
### v2 to v3 migration
|
|
306
|
+
|
|
307
|
+
v3.0.0 replaced the `retry_strategy: Callable` constructor parameter with `retry_budget_seconds`, `retry_max_delay_seconds`, and `retry_initial_delay_seconds`. Users with custom retry strategies should subclass `AbstractRedisGateway` instead (see [Custom gateway](#custom-gateway)).
|
|
308
|
+
|
|
293
309
|
## Running locally
|
|
294
310
|
|
|
295
311
|
You'll need a Redis server:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
|
|
2
|
+
from redis_message_queue._redis_gateway import RedisGateway
|
|
2
3
|
from redis_message_queue._stored_message import ClaimedMessage, MessageData
|
|
3
4
|
from redis_message_queue.interrupt_handler import (
|
|
4
5
|
BaseGracefulInterruptHandler,
|
|
@@ -8,6 +9,7 @@ from redis_message_queue.redis_message_queue import RedisMessageQueue
|
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"RedisMessageQueue",
|
|
12
|
+
"RedisGateway",
|
|
11
13
|
"AbstractRedisGateway",
|
|
12
14
|
"ClaimedMessage",
|
|
13
15
|
"MessageData",
|
|
@@ -128,6 +128,10 @@ class AbstractRedisGateway(ABC):
|
|
|
128
128
|
use leases.
|
|
129
129
|
|
|
130
130
|
Return None if no message was available (e.g. timeout or interrupt).
|
|
131
|
+
|
|
132
|
+
Implementations MUST respect a reasonable timeout or return None
|
|
133
|
+
periodically so the consumer can check for interrupts. Blocking
|
|
134
|
+
indefinitely without returning prevents graceful shutdown.
|
|
131
135
|
"""
|
|
132
136
|
|
|
133
137
|
@abstractmethod
|
|
@@ -36,13 +36,13 @@ def is_redis_retryable_exception(exception):
|
|
|
36
36
|
),
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
# 2. Explicit retryable exceptions
|
|
39
|
+
# 2. Explicit retryable exceptions (BusyLoadingError is a ConnectionError
|
|
40
|
+
# subclass, so it is already handled by branch 1 above)
|
|
40
41
|
return isinstance(
|
|
41
42
|
exception,
|
|
42
43
|
(
|
|
43
44
|
# Network/availability issues
|
|
44
45
|
redis.exceptions.TimeoutError, # Socket or server-side timeout
|
|
45
|
-
redis.exceptions.BusyLoadingError, # Server loading data
|
|
46
46
|
# Cluster transient failures
|
|
47
47
|
redis.exceptions.ClusterDownError, # Covers ClusterDown + MasterDown
|
|
48
48
|
redis.exceptions.TryAgainError, # Cluster state requires retry
|
|
@@ -275,6 +275,7 @@ if cached_result then
|
|
|
275
275
|
return tonumber(cached_result)
|
|
276
276
|
end
|
|
277
277
|
|
|
278
|
+
redis.call('LPUSH', KEYS[2], ARGV[2])
|
|
278
279
|
local removed = redis.call('LREM', KEYS[1], 1, ARGV[1])
|
|
279
280
|
if removed == 1 then
|
|
280
281
|
local claim_id = redis.call('HGET', KEYS[4], ARGV[1])
|
|
@@ -282,7 +283,8 @@ if removed == 1 then
|
|
|
282
283
|
redis.call('HDEL', KEYS[3], claim_id)
|
|
283
284
|
redis.call('HDEL', KEYS[4], ARGV[1])
|
|
284
285
|
end
|
|
285
|
-
|
|
286
|
+
else
|
|
287
|
+
redis.call('LREM', KEYS[2], 1, ARGV[2])
|
|
286
288
|
end
|
|
287
289
|
|
|
288
290
|
redis.call('SET', KEYS[5], tostring(removed), 'PX', tonumber(ARGV[3]))
|
|
@@ -455,6 +457,9 @@ local function redis_message_queue_decode_claim(cached_claim)
|
|
|
455
457
|
return nil
|
|
456
458
|
end
|
|
457
459
|
|
|
460
|
+
local time = redis.call('TIME')
|
|
461
|
+
local now_ms = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
462
|
+
|
|
458
463
|
-- Cache replay paths below return the ORIGINAL claim (same lease_token) even if
|
|
459
464
|
-- the lease deadline has passed in wall-clock time. Safe because ack is gated by
|
|
460
465
|
-- the server-side HGET lease_tokens check in MOVE/REMOVE_WITH_LEASE_TOKEN: if
|
|
@@ -469,6 +474,7 @@ if cached_claim then
|
|
|
469
474
|
redis.call('HSET', KEYS[10], ARGV[4], cached_claim)
|
|
470
475
|
redis.call('HSET', KEYS[11], claim[2], ARGV[4])
|
|
471
476
|
redis.call('HSET', KEYS[9], claim[2], KEYS[8])
|
|
477
|
+
redis.call('ZADD', KEYS[3], now_ms + tonumber(ARGV[1]), claim[1])
|
|
472
478
|
return {claim[1], claim[2]}
|
|
473
479
|
end
|
|
474
480
|
redis.call('DEL', KEYS[8])
|
|
@@ -481,14 +487,12 @@ if cached_recovery then
|
|
|
481
487
|
redis.call('SET', KEYS[8], cached_recovery, 'PX', tonumber(ARGV[3]))
|
|
482
488
|
redis.call('HSET', KEYS[11], claim[2], ARGV[4])
|
|
483
489
|
redis.call('HSET', KEYS[9], claim[2], KEYS[8])
|
|
490
|
+
redis.call('ZADD', KEYS[3], now_ms + tonumber(ARGV[1]), claim[1])
|
|
484
491
|
return {claim[1], claim[2]}
|
|
485
492
|
end
|
|
486
493
|
redis.call('HDEL', KEYS[10], ARGV[4])
|
|
487
494
|
end
|
|
488
495
|
|
|
489
|
-
local time = redis.call('TIME')
|
|
490
|
-
local now_ms = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
491
|
-
|
|
492
496
|
-- Cap at 100 to bound Lua execution time (Redis blocks during scripts).
|
|
493
497
|
-- With a single consumer polling at default interval, 1000 expired leases drain in ~2.5s.
|
|
494
498
|
local expired = redis.call('ZRANGEBYSCORE', KEYS[3], '-inf', now_ms, 'LIMIT', 0, 100)
|
|
@@ -518,15 +522,24 @@ if #to_requeue > 0 then
|
|
|
518
522
|
end
|
|
519
523
|
|
|
520
524
|
local function store_claim_and_return(stored)
|
|
521
|
-
|
|
522
|
-
local
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
525
|
+
-- pcall guards against OOM mid-write: compensate by returning message to pending
|
|
526
|
+
local ok, result = pcall(function()
|
|
527
|
+
local lease_token = tostring(redis.call('INCR', KEYS[5]))
|
|
528
|
+
local claim_payload = cjson.encode({stored, lease_token})
|
|
529
|
+
redis.call('ZADD', KEYS[3], now_ms + tonumber(ARGV[1]), stored)
|
|
530
|
+
redis.call('HSET', KEYS[4], stored, lease_token)
|
|
531
|
+
redis.call('SET', KEYS[8], claim_payload, 'PX', tonumber(ARGV[3]))
|
|
532
|
+
redis.call('HSET', KEYS[9], lease_token, KEYS[8])
|
|
533
|
+
redis.call('HSET', KEYS[10], ARGV[4], claim_payload)
|
|
534
|
+
redis.call('HSET', KEYS[11], lease_token, ARGV[4])
|
|
535
|
+
return {stored, lease_token}
|
|
536
|
+
end)
|
|
537
|
+
if not ok then
|
|
538
|
+
redis.call('LREM', KEYS[2], 1, stored)
|
|
539
|
+
redis.pcall('RPUSH', KEYS[1], stored)
|
|
540
|
+
return false
|
|
541
|
+
end
|
|
542
|
+
return result
|
|
530
543
|
end
|
|
531
544
|
|
|
532
545
|
local claim_attempts = 0
|
|
@@ -705,6 +718,7 @@ end
|
|
|
705
718
|
|
|
706
719
|
-- See REMOVE_MESSAGE_WITH_LEASE_TOKEN_LUA_SCRIPT for the bounded-leak rationale
|
|
707
720
|
-- on the removed == 0 branch (externally-removed message + valid lease token).
|
|
721
|
+
redis.call('LPUSH', KEYS[2], ARGV[2])
|
|
708
722
|
local removed = redis.call('LREM', KEYS[1], 1, ARGV[1])
|
|
709
723
|
if removed == 1 then
|
|
710
724
|
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
@@ -720,8 +734,9 @@ if removed == 1 then
|
|
|
720
734
|
redis.call('HDEL', KEYS[8], ARGV[3])
|
|
721
735
|
end
|
|
722
736
|
redis.call('HDEL', KEYS[5], ARGV[1])
|
|
723
|
-
redis.call('LPUSH', KEYS[2], ARGV[2])
|
|
724
737
|
redis.call('SET', KEYS[9], '1', 'PX', tonumber(ARGV[4]))
|
|
738
|
+
else
|
|
739
|
+
redis.call('LREM', KEYS[2], 1, ARGV[2])
|
|
725
740
|
end
|
|
726
741
|
|
|
727
742
|
return removed
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_queue_key_manager.py
RENAMED
|
@@ -23,6 +23,8 @@ class QueueKeyManager:
|
|
|
23
23
|
raise TypeError(f"'name' must be a string, got {type(queue_name).__name__}")
|
|
24
24
|
if not queue_name.strip():
|
|
25
25
|
raise ValueError("'name' must be a non-empty string")
|
|
26
|
+
if "\x00" in queue_name:
|
|
27
|
+
raise ValueError("queue name must not contain null bytes")
|
|
26
28
|
if not isinstance(key_separator, str):
|
|
27
29
|
raise TypeError(f"'key_separator' must be a string, got {type(key_separator).__name__}")
|
|
28
30
|
if not key_separator.strip():
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -88,7 +88,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
88
88
|
if isinstance(redis_client, redis.asyncio.Redis):
|
|
89
89
|
raise TypeError(
|
|
90
90
|
"'redis_client' is an async Redis client (redis.asyncio.Redis); "
|
|
91
|
-
"use the async
|
|
91
|
+
"use the async RedisMessageQueue from redis_message_queue.asyncio instead"
|
|
92
92
|
)
|
|
93
93
|
if isinstance(redis_client, (redis.client.Pipeline, redis.asyncio.client.Pipeline)):
|
|
94
94
|
raise TypeError(
|
|
@@ -380,6 +380,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
380
380
|
claimed_message = claim_message(from_queue, to_queue, claim_id)
|
|
381
381
|
except Exception as exc:
|
|
382
382
|
if not is_redis_retryable_exception(exc):
|
|
383
|
+
pending_claim_id_to_share = claim_id
|
|
383
384
|
raise
|
|
384
385
|
claim_may_need_recovery = True
|
|
385
386
|
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
@@ -414,8 +415,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
414
415
|
claimed_message = claim_message(from_queue, to_queue, claim_id)
|
|
415
416
|
except Exception as exc:
|
|
416
417
|
if not is_redis_retryable_exception(exc):
|
|
417
|
-
|
|
418
|
-
pending_claim_id_to_share = claim_id
|
|
418
|
+
pending_claim_id_to_share = claim_id
|
|
419
419
|
raise
|
|
420
420
|
claim_may_need_recovery = True
|
|
421
421
|
logger.warning(polling_retry_log, type(exc).__name__)
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/asyncio/__init__.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from redis_message_queue._stored_message import ClaimedMessage, MessageData
|
|
2
2
|
from redis_message_queue.asyncio._abstract_redis_gateway import AbstractRedisGateway
|
|
3
|
+
from redis_message_queue.asyncio._redis_gateway import RedisGateway
|
|
3
4
|
from redis_message_queue.asyncio.redis_message_queue import RedisMessageQueue
|
|
4
5
|
|
|
5
|
-
__all__ = ["RedisMessageQueue", "AbstractRedisGateway", "ClaimedMessage", "MessageData"]
|
|
6
|
+
__all__ = ["RedisMessageQueue", "RedisGateway", "AbstractRedisGateway", "ClaimedMessage", "MessageData"]
|
|
@@ -128,6 +128,10 @@ class AbstractRedisGateway(ABC):
|
|
|
128
128
|
use leases.
|
|
129
129
|
|
|
130
130
|
Return None if no message was available (e.g. timeout or interrupt).
|
|
131
|
+
|
|
132
|
+
Implementations MUST respect a reasonable timeout or return None
|
|
133
|
+
periodically so the consumer can check for interrupts. Blocking
|
|
134
|
+
indefinitely without returning prevents graceful shutdown.
|
|
131
135
|
"""
|
|
132
136
|
|
|
133
137
|
@abstractmethod
|
|
@@ -88,7 +88,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
88
88
|
if isinstance(redis_client, redis.Redis) and not isinstance(redis_client, redis.asyncio.Redis):
|
|
89
89
|
raise TypeError(
|
|
90
90
|
"'redis_client' is a sync Redis client (redis.Redis); "
|
|
91
|
-
"use the sync
|
|
91
|
+
"use the sync RedisMessageQueue from redis_message_queue instead"
|
|
92
92
|
)
|
|
93
93
|
if isinstance(redis_client, (redis.client.Pipeline, redis.asyncio.client.Pipeline)):
|
|
94
94
|
raise TypeError(
|
|
@@ -380,6 +380,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
380
380
|
claimed_message = await claim_message(from_queue, to_queue, claim_id)
|
|
381
381
|
except Exception as exc:
|
|
382
382
|
if not is_redis_retryable_exception(exc):
|
|
383
|
+
pending_claim_id_to_share = claim_id
|
|
383
384
|
raise
|
|
384
385
|
claim_may_need_recovery = True
|
|
385
386
|
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
@@ -415,8 +416,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
415
416
|
claimed_message = await claim_message(from_queue, to_queue, claim_id)
|
|
416
417
|
except Exception as exc:
|
|
417
418
|
if not is_redis_retryable_exception(exc):
|
|
418
|
-
|
|
419
|
-
pending_claim_id_to_share = claim_id
|
|
419
|
+
pending_claim_id_to_share = claim_id
|
|
420
420
|
raise
|
|
421
421
|
claim_may_need_recovery = True
|
|
422
422
|
logger.warning(polling_retry_log, type(exc).__name__)
|
|
@@ -327,7 +327,7 @@ class RedisMessageQueue:
|
|
|
327
327
|
max_failed_length: int | None = None,
|
|
328
328
|
max_delivery_count: int | None = None,
|
|
329
329
|
key_separator: str = "::",
|
|
330
|
-
get_deduplication_key: Optional[Callable] = None,
|
|
330
|
+
get_deduplication_key: Optional[Callable[[str | dict], str]] = None,
|
|
331
331
|
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
332
332
|
on_heartbeat_failure: Callable[[], Awaitable[None] | None] | None = None,
|
|
333
333
|
):
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -270,7 +270,7 @@ class RedisMessageQueue:
|
|
|
270
270
|
max_failed_length: int | None = None,
|
|
271
271
|
max_delivery_count: int | None = None,
|
|
272
272
|
key_separator: str = "::",
|
|
273
|
-
get_deduplication_key: Optional[Callable] = None,
|
|
273
|
+
get_deduplication_key: Optional[Callable[[str | dict], str]] = None,
|
|
274
274
|
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
275
275
|
on_heartbeat_failure: Callable[[], None] | None = None,
|
|
276
276
|
):
|
|
@@ -314,7 +314,7 @@ class RedisMessageQueue:
|
|
|
314
314
|
)
|
|
315
315
|
if get_deduplication_key is not None and not callable(get_deduplication_key):
|
|
316
316
|
raise TypeError(f"'get_deduplication_key' must be callable, got {type(get_deduplication_key).__name__}")
|
|
317
|
-
if get_deduplication_key is not None and
|
|
317
|
+
if get_deduplication_key is not None and is_async_callable(get_deduplication_key):
|
|
318
318
|
raise TypeError(
|
|
319
319
|
"'get_deduplication_key' is an async callable; "
|
|
320
320
|
"use the async RedisMessageQueue from redis_message_queue.asyncio instead"
|
|
File without changes
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-3.1.1 → redis_message_queue-3.1.2}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|