redis-message-queue 8.2.6__tar.gz → 8.2.8__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-8.2.6 → redis_message_queue-8.2.8}/PKG-INFO +14 -8
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/README.md +13 -7
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/pyproject.toml +2 -2
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_config.py +16 -3
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_exceptions.py +3 -2
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_redis_gateway.py +2 -1
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/asyncio/_redis_gateway.py +2 -1
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/asyncio/redis_message_queue.py +20 -5
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/redis_message_queue.py +25 -3
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/.gitignore +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/LICENSE +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_event.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_payload_limits.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/interrupt_handler/_event_driven.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redis-message-queue
|
|
3
|
-
Version: 8.2.
|
|
3
|
+
Version: 8.2.8
|
|
4
4
|
Summary: Python message queuing with Redis and message deduplication
|
|
5
5
|
Project-URL: Homepage, https://github.com/Elijas/redis-message-queue
|
|
6
6
|
Project-URL: Repository, https://github.com/Elijas/redis-message-queue
|
|
@@ -34,7 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
**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.
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
pip install "redis-message-queue>=8.2.
|
|
37
|
+
pip install "redis-message-queue>=8.2.8,<9.0.0"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Requires Redis server >= 6.2.
|
|
@@ -129,9 +129,12 @@ All features are optional and can be enabled or disabled as needed.
|
|
|
129
129
|
| Configuration | Delivery guarantee |
|
|
130
130
|
|---|---|
|
|
131
131
|
| Default (`visibility_timeout_seconds=300`) | **At-least-once** — expired messages are reclaimed and redelivered |
|
|
132
|
-
| With `visibility_timeout_seconds=None` | **At-most-once** — a consumer crash loses the in-flight message |
|
|
132
|
+
| With `visibility_timeout_seconds=None, max_delivery_count=None` | **At-most-once** — a consumer crash loses the in-flight message |
|
|
133
133
|
|
|
134
134
|
See [Crash recovery with visibility timeout](#crash-recovery-with-visibility-timeout) for details and tradeoffs.
|
|
135
|
+
Because delivery-count limits depend on visibility-timeout reclaim, disabling
|
|
136
|
+
lease-based crash recovery requires setting both `visibility_timeout_seconds=None`
|
|
137
|
+
and `max_delivery_count=None`.
|
|
135
138
|
|
|
136
139
|
> **Important:** Handler exceptions are terminal. This library is a payload
|
|
137
140
|
> queue, not a task framework: raising inside `process_message()` does not
|
|
@@ -290,7 +293,10 @@ queue = RedisMessageQueue(
|
|
|
290
293
|
|
|
291
294
|
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`.
|
|
292
295
|
|
|
293
|
-
|
|
296
|
+
With `visibility_timeout_seconds=None, max_delivery_count=None`, messages
|
|
297
|
+
already moved to `processing` remain there indefinitely after a consumer crash
|
|
298
|
+
and are not redelivered, even if the crash happened before your handler
|
|
299
|
+
started running.
|
|
294
300
|
|
|
295
301
|
Visibility deadlines use Redis server time (`TIME`), not Python process time.
|
|
296
302
|
A forward step in the Redis server clock can make a live lease appear expired
|
|
@@ -473,10 +479,10 @@ without `aclose()`, or sync processes killed mid-handler, can leave the message
|
|
|
473
479
|
and its processing/lease metadata in Redis until a later consumer claim path
|
|
474
480
|
triggers visibility-timeout reclaim. With visibility timeouts enabled, this is
|
|
475
481
|
the designed at-least-once recovery path: the message is delayed by the lease,
|
|
476
|
-
not lost. With `visibility_timeout_seconds=None`,
|
|
477
|
-
path. For low-visibility-timeout workloads,
|
|
478
|
-
`aclose()` during shutdown so local pending claim
|
|
479
|
-
process exit.
|
|
482
|
+
not lost. With `visibility_timeout_seconds=None, max_delivery_count=None`,
|
|
483
|
+
there is no automatic reclaim path. For low-visibility-timeout workloads,
|
|
484
|
+
prefer an explicit `drain()` / `aclose()` during shutdown so local pending claim
|
|
485
|
+
IDs are recovered before process exit.
|
|
480
486
|
|
|
481
487
|
`drain()` / `aclose()` timeouts are measured with Python monotonic clocks, but
|
|
482
488
|
any lease deadlines they recover were created from Redis server time. The same
|
|
@@ -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>=8.2.
|
|
14
|
+
pip install "redis-message-queue>=8.2.8,<9.0.0"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Requires Redis server >= 6.2.
|
|
@@ -106,9 +106,12 @@ All features are optional and can be enabled or disabled as needed.
|
|
|
106
106
|
| Configuration | Delivery guarantee |
|
|
107
107
|
|---|---|
|
|
108
108
|
| Default (`visibility_timeout_seconds=300`) | **At-least-once** — expired messages are reclaimed and redelivered |
|
|
109
|
-
| With `visibility_timeout_seconds=None` | **At-most-once** — a consumer crash loses the in-flight message |
|
|
109
|
+
| With `visibility_timeout_seconds=None, max_delivery_count=None` | **At-most-once** — a consumer crash loses the in-flight message |
|
|
110
110
|
|
|
111
111
|
See [Crash recovery with visibility timeout](#crash-recovery-with-visibility-timeout) for details and tradeoffs.
|
|
112
|
+
Because delivery-count limits depend on visibility-timeout reclaim, disabling
|
|
113
|
+
lease-based crash recovery requires setting both `visibility_timeout_seconds=None`
|
|
114
|
+
and `max_delivery_count=None`.
|
|
112
115
|
|
|
113
116
|
> **Important:** Handler exceptions are terminal. This library is a payload
|
|
114
117
|
> queue, not a task framework: raising inside `process_message()` does not
|
|
@@ -267,7 +270,10 @@ queue = RedisMessageQueue(
|
|
|
267
270
|
|
|
268
271
|
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`.
|
|
269
272
|
|
|
270
|
-
|
|
273
|
+
With `visibility_timeout_seconds=None, max_delivery_count=None`, messages
|
|
274
|
+
already moved to `processing` remain there indefinitely after a consumer crash
|
|
275
|
+
and are not redelivered, even if the crash happened before your handler
|
|
276
|
+
started running.
|
|
271
277
|
|
|
272
278
|
Visibility deadlines use Redis server time (`TIME`), not Python process time.
|
|
273
279
|
A forward step in the Redis server clock can make a live lease appear expired
|
|
@@ -450,10 +456,10 @@ without `aclose()`, or sync processes killed mid-handler, can leave the message
|
|
|
450
456
|
and its processing/lease metadata in Redis until a later consumer claim path
|
|
451
457
|
triggers visibility-timeout reclaim. With visibility timeouts enabled, this is
|
|
452
458
|
the designed at-least-once recovery path: the message is delayed by the lease,
|
|
453
|
-
not lost. With `visibility_timeout_seconds=None`,
|
|
454
|
-
path. For low-visibility-timeout workloads,
|
|
455
|
-
`aclose()` during shutdown so local pending claim
|
|
456
|
-
process exit.
|
|
459
|
+
not lost. With `visibility_timeout_seconds=None, max_delivery_count=None`,
|
|
460
|
+
there is no automatic reclaim path. For low-visibility-timeout workloads,
|
|
461
|
+
prefer an explicit `drain()` / `aclose()` during shutdown so local pending claim
|
|
462
|
+
IDs are recovered before process exit.
|
|
457
463
|
|
|
458
464
|
`drain()` / `aclose()` timeouts are measured with Python monotonic clocks, but
|
|
459
465
|
any lease deadlines they recover were created from Redis server time. The same
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "redis-message-queue"
|
|
3
|
-
version = "8.2.
|
|
3
|
+
version = "8.2.8"
|
|
4
4
|
description = "Python message queuing with Redis and message deduplication"
|
|
5
5
|
authors = [{ name = "Elijas", email = "4084885+Elijas@users.noreply.github.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -48,7 +48,7 @@ default-groups = ["dev", "test"]
|
|
|
48
48
|
##############################
|
|
49
49
|
|
|
50
50
|
[tool.bumpversion]
|
|
51
|
-
current_version = "8.2.
|
|
51
|
+
current_version = "8.2.8"
|
|
52
52
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
53
53
|
serialize = ["{major}.{minor}.{patch}"]
|
|
54
54
|
search = "{current_version}"
|
|
@@ -891,10 +891,11 @@ local dead_lettered_events = {}
|
|
|
891
891
|
local claim_store_failed_sentinel = string.char(0) .. '__rmq_claim_store_failed__'
|
|
892
892
|
|
|
893
893
|
local function store_claim_and_return(stored)
|
|
894
|
-
-- pcall guards against OOM mid-write:
|
|
894
|
+
-- pcall guards against OOM mid-write: fail fast while preserving a live payload copy.
|
|
895
|
+
local lease_token = nil
|
|
895
896
|
local ok, result = pcall(function()
|
|
896
897
|
redis.call('INCR', KEYS[5])
|
|
897
|
-
|
|
898
|
+
lease_token = redis.call('GET', KEYS[5])
|
|
898
899
|
local claim_payload = cjson.encode({stored, lease_token})
|
|
899
900
|
redis.call('ZADD', KEYS[3], now_ms + tonumber(ARGV[1]), stored)
|
|
900
901
|
redis.call('HSET', KEYS[4], stored, lease_token)
|
|
@@ -906,8 +907,20 @@ local function store_claim_and_return(stored)
|
|
|
906
907
|
end)
|
|
907
908
|
if not ok then
|
|
908
909
|
redis.call('HINCRBY', KEYS[6], stored, -1)
|
|
910
|
+
local return_result = redis.pcall('RPUSH', KEYS[1], stored)
|
|
911
|
+
if type(return_result) == 'table' and return_result['err'] then
|
|
912
|
+
local failure = tostring(result) .. '; return-to-pending failed: ' .. tostring(return_result['err'])
|
|
913
|
+
return {claim_store_failed_sentinel, failure, stored}
|
|
914
|
+
end
|
|
909
915
|
redis.call('LREM', KEYS[2], 1, stored)
|
|
910
|
-
redis.
|
|
916
|
+
redis.call('ZREM', KEYS[3], stored)
|
|
917
|
+
redis.call('HDEL', KEYS[4], stored)
|
|
918
|
+
redis.call('DEL', KEYS[8])
|
|
919
|
+
redis.call('HDEL', KEYS[10], ARGV[4])
|
|
920
|
+
if lease_token then
|
|
921
|
+
redis.call('HDEL', KEYS[9], lease_token)
|
|
922
|
+
redis.call('HDEL', KEYS[11], lease_token)
|
|
923
|
+
end
|
|
911
924
|
return {claim_store_failed_sentinel, tostring(result), stored}
|
|
912
925
|
end
|
|
913
926
|
return result
|
|
@@ -51,8 +51,9 @@ class CleanupFailedError(RedisMessageQueueError):
|
|
|
51
51
|
class ClaimStoreFailedError(RedisMessageQueueError):
|
|
52
52
|
"""Raised when the VT-claim Lua store_claim_and_return pcall failed.
|
|
53
53
|
|
|
54
|
-
The script decremented the speculative delivery_count increment
|
|
55
|
-
|
|
54
|
+
The script decremented the speculative delivery_count increment. It returns
|
|
55
|
+
the message to pending when that write succeeds; if that write fails, it
|
|
56
|
+
leaves the message in processing so the payload still has a live queue copy.
|
|
56
57
|
"""
|
|
57
58
|
|
|
58
59
|
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -987,7 +987,8 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
987
987
|
stored_message = result[2] if len(result) > 2 else None
|
|
988
988
|
message_id = extract_stored_message_id(stored_message) if isinstance(stored_message, (str, bytes)) else None
|
|
989
989
|
raise ClaimStoreFailedError(
|
|
990
|
-
f"VT claim store failed after delivery_count rollback:
|
|
990
|
+
f"VT claim store failed after delivery_count rollback and payload preservation: "
|
|
991
|
+
f"{_decode_lua_error(result[1])}",
|
|
991
992
|
queue=from_queue,
|
|
992
993
|
message_id=message_id,
|
|
993
994
|
operation="claim",
|
|
@@ -967,7 +967,8 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
967
967
|
stored_message = result[2] if len(result) > 2 else None
|
|
968
968
|
message_id = extract_stored_message_id(stored_message) if isinstance(stored_message, (str, bytes)) else None
|
|
969
969
|
raise ClaimStoreFailedError(
|
|
970
|
-
f"VT claim store failed after delivery_count rollback:
|
|
970
|
+
f"VT claim store failed after delivery_count rollback and payload preservation: "
|
|
971
|
+
f"{_decode_lua_error(result[1])}",
|
|
971
972
|
queue=from_queue,
|
|
972
973
|
message_id=message_id,
|
|
973
974
|
operation="claim",
|
|
@@ -607,8 +607,9 @@ class RedisMessageQueue:
|
|
|
607
607
|
):
|
|
608
608
|
"""Create a queue bound to an async Redis client or custom gateway.
|
|
609
609
|
|
|
610
|
-
``visibility_timeout_seconds`` defaults to 300.
|
|
611
|
-
|
|
610
|
+
``visibility_timeout_seconds`` defaults to 300. To disable
|
|
611
|
+
lease-based crash recovery, set both ``visibility_timeout_seconds=None``
|
|
612
|
+
and ``max_delivery_count=None``; messages left in ``processing`` by a
|
|
612
613
|
crashed worker are then not reclaimed automatically.
|
|
613
614
|
|
|
614
615
|
``visibility_timeout_seconds`` is a Redis server-time lease, not a
|
|
@@ -1072,9 +1073,23 @@ class RedisMessageQueue:
|
|
|
1072
1073
|
"See AbstractRedisGateway.add_message for the full contract."
|
|
1073
1074
|
)
|
|
1074
1075
|
else:
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1076
|
+
try:
|
|
1077
|
+
dedup_key = self._get_deduplication_key(message)
|
|
1078
|
+
if inspect.isawaitable(dedup_key):
|
|
1079
|
+
dedup_key = await dedup_key
|
|
1080
|
+
except asyncio.CancelledError as exc:
|
|
1081
|
+
current_task = asyncio.current_task()
|
|
1082
|
+
if current_task is not None and current_task.cancelling() > 0:
|
|
1083
|
+
raise
|
|
1084
|
+
_set_exception_context(exc, queue=self._queue_name, operation="publish")
|
|
1085
|
+
await self._emit_event(
|
|
1086
|
+
"publish",
|
|
1087
|
+
"failure",
|
|
1088
|
+
exception_type=type(exc).__name__,
|
|
1089
|
+
error=exc,
|
|
1090
|
+
duration_ms=_duration_ms(started_at),
|
|
1091
|
+
)
|
|
1092
|
+
raise
|
|
1078
1093
|
dedup_key = validate_callable_deduplication_key(dedup_key, message)
|
|
1079
1094
|
dedup_key = self.key.deduplication(dedup_key)
|
|
1080
1095
|
result = await self._redis.publish_message(self.key.pending, message_str, dedup_key)
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -80,6 +80,14 @@ def _duration_ms(started_at: float) -> float:
|
|
|
80
80
|
return (time.perf_counter() - started_at) * 1000
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def _current_async_task_is_cancelling() -> bool:
|
|
84
|
+
try:
|
|
85
|
+
current_task = asyncio.current_task()
|
|
86
|
+
except RuntimeError:
|
|
87
|
+
return False
|
|
88
|
+
return current_task is not None and current_task.cancelling() > 0
|
|
89
|
+
|
|
90
|
+
|
|
83
91
|
def _hash_lease_token(lease_token: str | None) -> str | None:
|
|
84
92
|
if lease_token is None:
|
|
85
93
|
return None
|
|
@@ -558,8 +566,9 @@ class RedisMessageQueue:
|
|
|
558
566
|
):
|
|
559
567
|
"""Create a queue bound to a Redis client or custom gateway.
|
|
560
568
|
|
|
561
|
-
``visibility_timeout_seconds`` defaults to 300.
|
|
562
|
-
|
|
569
|
+
``visibility_timeout_seconds`` defaults to 300. To disable
|
|
570
|
+
lease-based crash recovery, set both ``visibility_timeout_seconds=None``
|
|
571
|
+
and ``max_delivery_count=None``; messages left in ``processing`` by a
|
|
563
572
|
crashed worker are then not reclaimed automatically.
|
|
564
573
|
|
|
565
574
|
``visibility_timeout_seconds`` is a Redis server-time lease, not a
|
|
@@ -1017,7 +1026,20 @@ class RedisMessageQueue:
|
|
|
1017
1026
|
"See AbstractRedisGateway.add_message for the full contract."
|
|
1018
1027
|
)
|
|
1019
1028
|
else:
|
|
1020
|
-
|
|
1029
|
+
try:
|
|
1030
|
+
dedup_key = self._get_deduplication_key(message)
|
|
1031
|
+
except asyncio.CancelledError as exc:
|
|
1032
|
+
if _current_async_task_is_cancelling():
|
|
1033
|
+
raise
|
|
1034
|
+
_set_exception_context(exc, queue=self._queue_name, operation="publish")
|
|
1035
|
+
self._emit_event(
|
|
1036
|
+
"publish",
|
|
1037
|
+
"failure",
|
|
1038
|
+
exception_type=type(exc).__name__,
|
|
1039
|
+
error=exc,
|
|
1040
|
+
duration_ms=_duration_ms(started_at),
|
|
1041
|
+
)
|
|
1042
|
+
raise
|
|
1021
1043
|
if inspect.isawaitable(dedup_key):
|
|
1022
1044
|
is_coroutine = inspect.iscoroutine(dedup_key)
|
|
1023
1045
|
_close_or_cancel_awaitable(dedup_key)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_payload_limits.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.6 → redis_message_queue-8.2.8}/redis_message_queue/asyncio/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|