redis-message-queue 3.1.0__tar.gz → 3.1.1__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.0 → redis_message_queue-3.1.1}/PKG-INFO +15 -4
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/README.md +4 -3
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/pyproject.toml +17 -1
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_abstract_redis_gateway.py +4 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_config.py +2 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_redis_gateway.py +2 -2
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/asyncio/_abstract_redis_gateway.py +4 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/asyncio/_redis_gateway.py +2 -2
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/asyncio/redis_message_queue.py +1 -1
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/interrupt_handler/_implementation.py +10 -2
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/redis_message_queue.py +1 -1
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/LICENSE +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/interrupt_handler/_interface.py +0 -0
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redis-message-queue
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.1
|
|
4
4
|
Summary: Python message queuing with Redis and message deduplication
|
|
5
|
+
License: MIT
|
|
5
6
|
License-File: LICENSE
|
|
7
|
+
Keywords: redis,message-queue,deduplication,task-queue
|
|
6
8
|
Author: Elijas
|
|
7
9
|
Author-email: 4084885+Elijas@users.noreply.github.com
|
|
8
10
|
Requires-Python: >=3.12,<4.0
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
14
|
Classifier: Programming Language :: Python :: 3
|
|
10
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
13
20
|
Requires-Dist: redis (>=5.0.0)
|
|
14
21
|
Requires-Dist: tenacity (>=8.1.0)
|
|
22
|
+
Project-URL: Homepage, https://github.com/Elijas/redis-message-queue
|
|
23
|
+
Project-URL: Issues, https://github.com/Elijas/redis-message-queue/issues
|
|
24
|
+
Project-URL: Repository, https://github.com/Elijas/redis-message-queue
|
|
15
25
|
Description-Content-Type: text/markdown
|
|
16
26
|
|
|
17
27
|
# redis-message-queue
|
|
18
28
|
|
|
19
|
-
[](https://pypi.org/project/redis-message-queue)
|
|
20
30
|
[](https://pypistats.org/packages/redis-message-queue)
|
|
21
31
|
[](LICENSE)
|
|
22
32
|
[](https://github.com/Elijas/redis-message-queue/issues)
|
|
@@ -150,7 +160,7 @@ This enables lease-based redelivery for messages left in `processing` by a crash
|
|
|
150
160
|
Tradeoffs:
|
|
151
161
|
- delivery becomes at-least-once after lease expiry
|
|
152
162
|
- the timeout must be longer than your normal processing time if you do not use heartbeats
|
|
153
|
-
- if you do use heartbeats, the heartbeat interval must be
|
|
163
|
+
- if you do use heartbeats, the heartbeat interval must be less than half of the visibility timeout
|
|
154
164
|
- recovery happens on consumer polling cadence rather than instantly
|
|
155
165
|
- heartbeats add background renewal work for active messages
|
|
156
166
|
- 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
|
|
@@ -239,7 +249,7 @@ entirely (single attempt; exceptions propagate). The library uses
|
|
|
239
249
|
`retry_budget_seconds` to size the operation-result cache TTL automatically,
|
|
240
250
|
so the previous footgun of an over-long retry budget out-living the cache
|
|
241
251
|
and producing misleading "cleanup was a no-op" warnings is now structurally
|
|
242
|
-
impossible.
|
|
252
|
+
impossible. Note: tenacity may allow one additional attempt beyond the budget if the budget check passes at attempt start — total wall-clock time can exceed `retry_budget_seconds` by the duration of that final attempt.
|
|
243
253
|
|
|
244
254
|
To plug in a different retry library (`backoff`, `asyncstdlib.retry`, or your
|
|
245
255
|
own logic) or fundamentally different semantics, subclass
|
|
@@ -300,6 +310,7 @@ await client.aclose()
|
|
|
300
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).
|
|
301
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.
|
|
302
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
|
+
- **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.
|
|
303
314
|
- **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.
|
|
304
315
|
- **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`.
|
|
305
316
|
|
|
@@ -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)
|
|
@@ -134,7 +134,7 @@ This enables lease-based redelivery for messages left in `processing` by a crash
|
|
|
134
134
|
Tradeoffs:
|
|
135
135
|
- delivery becomes at-least-once after lease expiry
|
|
136
136
|
- the timeout must be longer than your normal processing time if you do not use heartbeats
|
|
137
|
-
- if you do use heartbeats, the heartbeat interval must be
|
|
137
|
+
- if you do use heartbeats, the heartbeat interval must be less than half of the visibility timeout
|
|
138
138
|
- recovery happens on consumer polling cadence rather than instantly
|
|
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
|
|
@@ -223,7 +223,7 @@ entirely (single attempt; exceptions propagate). The library uses
|
|
|
223
223
|
`retry_budget_seconds` to size the operation-result cache TTL automatically,
|
|
224
224
|
so the previous footgun of an over-long retry budget out-living the cache
|
|
225
225
|
and producing misleading "cleanup was a no-op" warnings is now structurally
|
|
226
|
-
impossible.
|
|
226
|
+
impossible. Note: tenacity may allow one additional attempt beyond the budget if the budget check passes at attempt start — total wall-clock time can exceed `retry_budget_seconds` by the duration of that final attempt.
|
|
227
227
|
|
|
228
228
|
To plug in a different retry library (`backoff`, `asyncstdlib.retry`, or your
|
|
229
229
|
own logic) or fundamentally different semantics, subclass
|
|
@@ -284,6 +284,7 @@ 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
|
+
- **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.
|
|
287
288
|
- **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.
|
|
288
289
|
- **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`.
|
|
289
290
|
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "redis-message-queue"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.1"
|
|
4
4
|
description = "Python message queuing with Redis and message deduplication"
|
|
5
5
|
authors = ["Elijas <4084885+Elijas@users.noreply.github.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = ["redis", "message-queue", "deduplication", "task-queue"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Topic :: Software Development :: Libraries",
|
|
16
|
+
"Topic :: System :: Distributed Computing",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.poetry.urls]
|
|
20
|
+
Homepage = "https://github.com/Elijas/redis-message-queue"
|
|
21
|
+
Repository = "https://github.com/Elijas/redis-message-queue"
|
|
22
|
+
Issues = "https://github.com/Elijas/redis-message-queue/issues"
|
|
7
23
|
|
|
8
24
|
[tool.poetry.dependencies]
|
|
9
25
|
python = "^3.12"
|
|
@@ -19,6 +19,10 @@ class AbstractRedisGateway(ABC):
|
|
|
19
19
|
silently disables heartbeat validation and lease-token safety checks,
|
|
20
20
|
causing the queue to treat the gateway as a non-lease implementation.
|
|
21
21
|
|
|
22
|
+
The queue also reads ``max_delivery_count`` and ``dead_letter_queue``
|
|
23
|
+
from the gateway via ``getattr``. Avoid using these attribute names for
|
|
24
|
+
unrelated purposes on custom gateway implementations.
|
|
25
|
+
|
|
22
26
|
Gateways that wrap a Redis Cluster client should expose an
|
|
23
27
|
``is_redis_cluster`` property returning ``True`` so the queue can apply
|
|
24
28
|
hash-tag validation at construction time.
|
|
@@ -179,6 +179,8 @@ def validate_dead_letter_parameters(
|
|
|
179
179
|
raise ValueError("'max_delivery_count' requires 'message_visibility_timeout_seconds' to be set.")
|
|
180
180
|
if dead_letter_queue is not None and not isinstance(dead_letter_queue, str):
|
|
181
181
|
raise TypeError(f"'dead_letter_queue' must be a str or None, got {type(dead_letter_queue).__name__}")
|
|
182
|
+
if isinstance(dead_letter_queue, str) and dead_letter_queue and not dead_letter_queue.strip():
|
|
183
|
+
raise ValueError("'dead_letter_queue' must be a non-empty string")
|
|
182
184
|
if max_delivery_count is not None and not dead_letter_queue:
|
|
183
185
|
raise ValueError("'dead_letter_queue' is required when 'max_delivery_count' is set.")
|
|
184
186
|
if dead_letter_queue and max_delivery_count is None:
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -382,7 +382,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
382
382
|
if not is_redis_retryable_exception(exc):
|
|
383
383
|
raise
|
|
384
384
|
claim_may_need_recovery = True
|
|
385
|
-
logger.warning(non_blocking_retry_log, exc)
|
|
385
|
+
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
386
386
|
if self._is_interrupted():
|
|
387
387
|
pending_claim_id_to_share = claim_id
|
|
388
388
|
return None
|
|
@@ -418,7 +418,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
418
418
|
pending_claim_id_to_share = claim_id
|
|
419
419
|
raise
|
|
420
420
|
claim_may_need_recovery = True
|
|
421
|
-
logger.warning(polling_retry_log, exc)
|
|
421
|
+
logger.warning(polling_retry_log, type(exc).__name__)
|
|
422
422
|
last_retryable_exception = exc
|
|
423
423
|
except BaseException:
|
|
424
424
|
pending_claim_id_to_share = claim_id
|
|
@@ -20,6 +20,10 @@ class AbstractRedisGateway(ABC):
|
|
|
20
20
|
silently disables heartbeat validation and lease-token safety checks,
|
|
21
21
|
causing the queue to treat the gateway as a non-lease implementation.
|
|
22
22
|
|
|
23
|
+
The queue also reads ``max_delivery_count`` and ``dead_letter_queue``
|
|
24
|
+
from the gateway via ``getattr``. Avoid using these attribute names for
|
|
25
|
+
unrelated purposes on custom gateway implementations.
|
|
26
|
+
|
|
23
27
|
Gateways that wrap a Redis Cluster client should expose an
|
|
24
28
|
``is_redis_cluster`` property returning ``True`` so the queue can apply
|
|
25
29
|
hash-tag validation at construction time.
|
|
@@ -382,7 +382,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
382
382
|
if not is_redis_retryable_exception(exc):
|
|
383
383
|
raise
|
|
384
384
|
claim_may_need_recovery = True
|
|
385
|
-
logger.warning(non_blocking_retry_log, exc)
|
|
385
|
+
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
386
386
|
if self._is_interrupted():
|
|
387
387
|
pending_claim_id_to_share = claim_id
|
|
388
388
|
return None
|
|
@@ -419,7 +419,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
419
419
|
pending_claim_id_to_share = claim_id
|
|
420
420
|
raise
|
|
421
421
|
claim_may_need_recovery = True
|
|
422
|
-
logger.warning(polling_retry_log, exc)
|
|
422
|
+
logger.warning(polling_retry_log, type(exc).__name__)
|
|
423
423
|
last_retryable_exception = exc
|
|
424
424
|
except BaseException:
|
|
425
425
|
pending_claim_id_to_share = claim_id
|
|
@@ -460,7 +460,7 @@ class RedisMessageQueue:
|
|
|
460
460
|
"'message' dict keys must all be strings; "
|
|
461
461
|
f"got non-string keys: {non_str_keys[:3]}" + (" (and more)" if len(non_str_keys) > 3 else "")
|
|
462
462
|
)
|
|
463
|
-
message_str = json.dumps(message, sort_keys=True)
|
|
463
|
+
message_str = json.dumps(message, sort_keys=True, allow_nan=False)
|
|
464
464
|
else:
|
|
465
465
|
message_str = message
|
|
466
466
|
|
|
@@ -61,7 +61,10 @@ class GracefulInterruptHandler(BaseGracefulInterruptHandler):
|
|
|
61
61
|
f"'signals' must contain signal.Signals members, got {type(sig).__name__} at position {i}"
|
|
62
62
|
)
|
|
63
63
|
for sig in signals:
|
|
64
|
-
|
|
64
|
+
try:
|
|
65
|
+
current = signal.getsignal(sig)
|
|
66
|
+
except OSError:
|
|
67
|
+
raise ValueError(f"Signal {sig.name} cannot be caught or handled by user code.")
|
|
65
68
|
if _is_graceful_interrupt_handler(current):
|
|
66
69
|
raise ValueError(
|
|
67
70
|
f"Signal {sig.name} is already owned by another GracefulInterruptHandler."
|
|
@@ -78,7 +81,12 @@ class GracefulInterruptHandler(BaseGracefulInterruptHandler):
|
|
|
78
81
|
self._signals = signals
|
|
79
82
|
self._previous_handlers = {sig: signal.getsignal(sig) for sig in self._signals}
|
|
80
83
|
for sig in self._signals:
|
|
81
|
-
|
|
84
|
+
try:
|
|
85
|
+
signal.signal(sig, self._signal_handler)
|
|
86
|
+
except ValueError as e:
|
|
87
|
+
if "main thread" in str(e):
|
|
88
|
+
raise ValueError("GracefulInterruptHandler must be created on the main thread.") from e
|
|
89
|
+
raise
|
|
82
90
|
|
|
83
91
|
def is_interrupted(self) -> bool:
|
|
84
92
|
return self._interrupted
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -414,7 +414,7 @@ class RedisMessageQueue:
|
|
|
414
414
|
"'message' dict keys must all be strings; "
|
|
415
415
|
f"got non-string keys: {non_str_keys[:3]}" + (" (and more)" if len(non_str_keys) > 3 else "")
|
|
416
416
|
)
|
|
417
|
-
message_str = json.dumps(message, sort_keys=True)
|
|
417
|
+
message_str = json.dumps(message, sort_keys=True, allow_nan=False)
|
|
418
418
|
else:
|
|
419
419
|
message_str = message
|
|
420
420
|
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-3.1.0 → redis_message_queue-3.1.1}/redis_message_queue/asyncio/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|