redis-message-queue 7.0.1__tar.gz → 8.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-7.0.1 → redis_message_queue-8.0.0}/PKG-INFO +77 -23
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/README.md +76 -22
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/pyproject.toml +1 -1
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_config.py +14 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/asyncio/redis_message_queue.py +15 -26
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/redis_message_queue.py +21 -33
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/LICENSE +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_event.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_exceptions.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_redis_gateway.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/asyncio/_redis_gateway.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/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:
|
|
3
|
+
Version: 8.0.0
|
|
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)
|
|
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
**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.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
pip install "redis-message-queue>=
|
|
40
|
+
pip install "redis-message-queue>=8.0.0,<9.0.0"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Requires Redis server >= 6.2.
|
|
@@ -52,11 +52,16 @@ from redis import Redis
|
|
|
52
52
|
from redis_message_queue import RedisMessageQueue
|
|
53
53
|
|
|
54
54
|
client = Redis.from_url("redis://localhost:6379/0", decode_responses=True)
|
|
55
|
-
queue = RedisMessageQueue(
|
|
56
|
-
|
|
55
|
+
queue = RedisMessageQueue(
|
|
56
|
+
"quickstart",
|
|
57
|
+
client=client,
|
|
58
|
+
deduplication=True,
|
|
59
|
+
get_deduplication_key=lambda msg: msg["id"],
|
|
60
|
+
)
|
|
61
|
+
queue.publish({"id": "msg-1", "text": "hello"})
|
|
57
62
|
with queue.process_message() as message:
|
|
58
63
|
if message is not None:
|
|
59
|
-
print(f"got {message}")
|
|
64
|
+
print(f"got {message['text']}")
|
|
60
65
|
# Expected output: got hello
|
|
61
66
|
```
|
|
62
67
|
|
|
@@ -72,10 +77,15 @@ from redis_message_queue.asyncio import RedisMessageQueue
|
|
|
72
77
|
|
|
73
78
|
async def main():
|
|
74
79
|
client = Redis.from_url("redis://localhost:6379/0", decode_responses=True)
|
|
75
|
-
queue = RedisMessageQueue(
|
|
76
|
-
|
|
80
|
+
queue = RedisMessageQueue(
|
|
81
|
+
"quickstart",
|
|
82
|
+
client=client,
|
|
83
|
+
deduplication=True,
|
|
84
|
+
get_deduplication_key=lambda msg: msg["id"],
|
|
85
|
+
)
|
|
86
|
+
await queue.publish({"id": "msg-1", "text": "hello"})
|
|
77
87
|
async with queue.process_message() as message:
|
|
78
|
-
print(f"got {message}")
|
|
88
|
+
print(f"got {message['text']}")
|
|
79
89
|
await client.aclose()
|
|
80
90
|
|
|
81
91
|
asyncio.run(main()) # Expected output: got hello
|
|
@@ -89,7 +99,7 @@ asyncio.run(main()) # Expected output: got hello
|
|
|
89
99
|
|
|
90
100
|
| Feature | Details |
|
|
91
101
|
|---------|---------|
|
|
92
|
-
| **Deduplicated publish** | Lua-scripted atomic SET NX + LPUSH prevents duplicate enqueues within a configurable TTL window (default: 1 hour), even with producer retries.
|
|
102
|
+
| **Deduplicated publish** | Lua-scripted atomic SET NX + LPUSH prevents duplicate enqueues within a configurable TTL window (default: 1 hour), even with producer retries. Requires an explicit `get_deduplication_key` callable so your application defines what counts as a duplicate. Note: deduplication is publish-side only and does not prevent duplicate *delivery* under at-least-once visibility-timeout reclaim |
|
|
93
103
|
| **Visibility-timeout redelivery** | Crashed or stalled consumers' messages are reclaimed and redelivered when a visibility timeout is configured |
|
|
94
104
|
| **Success & failure logs** | Optional completed/failed queues for auditing and reprocessing, with configurable max length to prevent unbounded growth |
|
|
95
105
|
| **Dead-letter queue** | Poison messages that exceed a configurable delivery count are automatically routed to a dead-letter queue instead of being redelivered indefinitely |
|
|
@@ -114,10 +124,7 @@ See [Crash recovery with visibility timeout](#crash-recovery-with-visibility-tim
|
|
|
114
124
|
### Deduplication
|
|
115
125
|
|
|
116
126
|
```python
|
|
117
|
-
#
|
|
118
|
-
queue = RedisMessageQueue("q", client=client, deduplication=True)
|
|
119
|
-
|
|
120
|
-
# Custom dedup key (e.g., deduplicate by order ID only)
|
|
127
|
+
# Deduplicate by order ID for a 1-hour TTL
|
|
121
128
|
queue = RedisMessageQueue(
|
|
122
129
|
"q", client=client,
|
|
123
130
|
deduplication=True,
|
|
@@ -128,12 +135,13 @@ queue = RedisMessageQueue(
|
|
|
128
135
|
queue = RedisMessageQueue("q", client=client, deduplication=False)
|
|
129
136
|
```
|
|
130
137
|
|
|
131
|
-
####
|
|
138
|
+
#### Dedup key callable must return a non-empty, high-cardinality, tenant-scoped string
|
|
132
139
|
|
|
133
|
-
When `get_deduplication_key` is
|
|
134
|
-
must return a `str` that uniquely represents the
|
|
135
|
-
message. Returning `None` or `""` raises
|
|
136
|
-
returning a non-`str` value raises
|
|
140
|
+
When `deduplication=True`, `get_deduplication_key` is required. The callable is
|
|
141
|
+
called once per publish and must return a `str` that uniquely represents the
|
|
142
|
+
deduplication scope for that message. Returning `None` or `""` raises
|
|
143
|
+
`ConfigurationError` at publish time; returning a non-`str` value raises
|
|
144
|
+
`TypeError`.
|
|
137
145
|
|
|
138
146
|
Use stable, high-cardinality keys that include any tenant or account boundary
|
|
139
147
|
needed by your system:
|
|
@@ -551,9 +559,9 @@ avoids the parent-construct hazard entirely.
|
|
|
551
559
|
### Redis memory sizing for deduplication and replay metadata
|
|
552
560
|
|
|
553
561
|
When deduplication is enabled, each distinct dedup key creates one Redis string
|
|
554
|
-
for `message_deduplication_log_ttl_seconds` (default: 3600 seconds). The
|
|
555
|
-
|
|
556
|
-
|
|
562
|
+
for `message_deduplication_log_ttl_seconds` (default: 3600 seconds). The dedup
|
|
563
|
+
key is whatever your `get_deduplication_key` callable returns, so choose a
|
|
564
|
+
short, stable logical ID and size Redis for:
|
|
557
565
|
|
|
558
566
|
```text
|
|
559
567
|
peak_unique_publish_rate_per_second
|
|
@@ -758,6 +766,52 @@ For a full analysis, see [docs/production-readiness.md](docs/production-readines
|
|
|
758
766
|
|
|
759
767
|
## Upgrading
|
|
760
768
|
|
|
769
|
+
### v7 to v8 migration
|
|
770
|
+
|
|
771
|
+
v8.0.0 removes implicit dedup key generation. Deduplication is opt-in and
|
|
772
|
+
`deduplication=True` now requires `get_deduplication_key`. If you were relying
|
|
773
|
+
on v7's automatic content hash, provide the equivalent callable explicitly.
|
|
774
|
+
|
|
775
|
+
Before:
|
|
776
|
+
|
|
777
|
+
```python
|
|
778
|
+
queue = RedisMessageQueue(
|
|
779
|
+
"orders",
|
|
780
|
+
client=client,
|
|
781
|
+
deduplication=True,
|
|
782
|
+
)
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
After, prefer a stable logical ID:
|
|
786
|
+
|
|
787
|
+
```python
|
|
788
|
+
queue = RedisMessageQueue(
|
|
789
|
+
"orders",
|
|
790
|
+
client=client,
|
|
791
|
+
deduplication=True,
|
|
792
|
+
get_deduplication_key=lambda msg: msg["order_id"],
|
|
793
|
+
)
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
To preserve v7 content-hash behavior mechanically:
|
|
797
|
+
|
|
798
|
+
```python
|
|
799
|
+
import hashlib
|
|
800
|
+
import json
|
|
801
|
+
|
|
802
|
+
queue = RedisMessageQueue(
|
|
803
|
+
"orders",
|
|
804
|
+
client=client,
|
|
805
|
+
deduplication=True,
|
|
806
|
+
get_deduplication_key=lambda msg: hashlib.sha256(
|
|
807
|
+
json.dumps(msg, sort_keys=True).encode()
|
|
808
|
+
).hexdigest(),
|
|
809
|
+
)
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
If you do not need publish-side deduplication, omit `deduplication` or set
|
|
813
|
+
`deduplication=False`.
|
|
814
|
+
|
|
761
815
|
### v6 to v7 migration
|
|
762
816
|
|
|
763
817
|
v7.0.0 has four breaking changes to check during upgrade.
|
|
@@ -845,7 +899,7 @@ Users on redis-py 7.x and earlier are unaffected. If you installed a redis-py
|
|
|
845
899
|
- **Do not toggle visibility timeout in either direction with messages in processing.** Messages claimed by non-VT consumers have no lease metadata, so VT-enabled consumers cannot reclaim them. Disabling VT later orphans existing lease deadline, lease token, and delivery count metadata and removes crash recovery for those in-flight messages. Drain the processing queue first.
|
|
846
900
|
- **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.
|
|
847
901
|
- **Changing `max_delivery_count` from a number to `None` leaves delivery metadata behind.** The delivery count hash continues to exist but is no longer consulted. Use this only after draining or after planning manual cleanup of the delivery-count hash.
|
|
848
|
-
- **Changing `get_deduplication_key` changes the dedup keyspace.** Existing dedup records become inert for the duration of their TTL. Drain the queue or clear the old deduplication keys before switching
|
|
902
|
+
- **Changing `get_deduplication_key` changes the dedup keyspace.** Existing dedup records become inert for the duration of their TTL. Drain the queue or clear the old deduplication keys before switching key functions.
|
|
849
903
|
- **Disabling `deduplication` has a retention-window overlap.** Existing dedup records remain in Redis until their TTL expires, but new publishes bypass them. Republishes that would have been suppressed under the old setting can enqueue during that window.
|
|
850
904
|
- **Disabling `enable_failed_queue` stops recording handler failures.** Existing failed entries remain in Redis, but new failures are removed from `processing` without being appended to the failed queue. If `max_delivery_count=None` is also set, repeated handler failures can be dropped with no DLQ or failed-queue record; see [Dead-letter queue](#dead-letter-queue).
|
|
851
905
|
- **Lowering `max_completed_length` or `max_failed_length` trims existing history.** The next completed or failed move calls `LTRIM`, so changing `None` to `N` or lowering `N` can immediately reduce historical entries to the new cap.
|
|
@@ -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>=8.0.0,<9.0.0"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Requires Redis server >= 6.2.
|
|
@@ -26,11 +26,16 @@ from redis import Redis
|
|
|
26
26
|
from redis_message_queue import RedisMessageQueue
|
|
27
27
|
|
|
28
28
|
client = Redis.from_url("redis://localhost:6379/0", decode_responses=True)
|
|
29
|
-
queue = RedisMessageQueue(
|
|
30
|
-
|
|
29
|
+
queue = RedisMessageQueue(
|
|
30
|
+
"quickstart",
|
|
31
|
+
client=client,
|
|
32
|
+
deduplication=True,
|
|
33
|
+
get_deduplication_key=lambda msg: msg["id"],
|
|
34
|
+
)
|
|
35
|
+
queue.publish({"id": "msg-1", "text": "hello"})
|
|
31
36
|
with queue.process_message() as message:
|
|
32
37
|
if message is not None:
|
|
33
|
-
print(f"got {message}")
|
|
38
|
+
print(f"got {message['text']}")
|
|
34
39
|
# Expected output: got hello
|
|
35
40
|
```
|
|
36
41
|
|
|
@@ -46,10 +51,15 @@ from redis_message_queue.asyncio import RedisMessageQueue
|
|
|
46
51
|
|
|
47
52
|
async def main():
|
|
48
53
|
client = Redis.from_url("redis://localhost:6379/0", decode_responses=True)
|
|
49
|
-
queue = RedisMessageQueue(
|
|
50
|
-
|
|
54
|
+
queue = RedisMessageQueue(
|
|
55
|
+
"quickstart",
|
|
56
|
+
client=client,
|
|
57
|
+
deduplication=True,
|
|
58
|
+
get_deduplication_key=lambda msg: msg["id"],
|
|
59
|
+
)
|
|
60
|
+
await queue.publish({"id": "msg-1", "text": "hello"})
|
|
51
61
|
async with queue.process_message() as message:
|
|
52
|
-
print(f"got {message}")
|
|
62
|
+
print(f"got {message['text']}")
|
|
53
63
|
await client.aclose()
|
|
54
64
|
|
|
55
65
|
asyncio.run(main()) # Expected output: got hello
|
|
@@ -63,7 +73,7 @@ asyncio.run(main()) # Expected output: got hello
|
|
|
63
73
|
|
|
64
74
|
| Feature | Details |
|
|
65
75
|
|---------|---------|
|
|
66
|
-
| **Deduplicated publish** | Lua-scripted atomic SET NX + LPUSH prevents duplicate enqueues within a configurable TTL window (default: 1 hour), even with producer retries.
|
|
76
|
+
| **Deduplicated publish** | Lua-scripted atomic SET NX + LPUSH prevents duplicate enqueues within a configurable TTL window (default: 1 hour), even with producer retries. Requires an explicit `get_deduplication_key` callable so your application defines what counts as a duplicate. Note: deduplication is publish-side only and does not prevent duplicate *delivery* under at-least-once visibility-timeout reclaim |
|
|
67
77
|
| **Visibility-timeout redelivery** | Crashed or stalled consumers' messages are reclaimed and redelivered when a visibility timeout is configured |
|
|
68
78
|
| **Success & failure logs** | Optional completed/failed queues for auditing and reprocessing, with configurable max length to prevent unbounded growth |
|
|
69
79
|
| **Dead-letter queue** | Poison messages that exceed a configurable delivery count are automatically routed to a dead-letter queue instead of being redelivered indefinitely |
|
|
@@ -88,10 +98,7 @@ See [Crash recovery with visibility timeout](#crash-recovery-with-visibility-tim
|
|
|
88
98
|
### Deduplication
|
|
89
99
|
|
|
90
100
|
```python
|
|
91
|
-
#
|
|
92
|
-
queue = RedisMessageQueue("q", client=client, deduplication=True)
|
|
93
|
-
|
|
94
|
-
# Custom dedup key (e.g., deduplicate by order ID only)
|
|
101
|
+
# Deduplicate by order ID for a 1-hour TTL
|
|
95
102
|
queue = RedisMessageQueue(
|
|
96
103
|
"q", client=client,
|
|
97
104
|
deduplication=True,
|
|
@@ -102,12 +109,13 @@ queue = RedisMessageQueue(
|
|
|
102
109
|
queue = RedisMessageQueue("q", client=client, deduplication=False)
|
|
103
110
|
```
|
|
104
111
|
|
|
105
|
-
####
|
|
112
|
+
#### Dedup key callable must return a non-empty, high-cardinality, tenant-scoped string
|
|
106
113
|
|
|
107
|
-
When `get_deduplication_key` is
|
|
108
|
-
must return a `str` that uniquely represents the
|
|
109
|
-
message. Returning `None` or `""` raises
|
|
110
|
-
returning a non-`str` value raises
|
|
114
|
+
When `deduplication=True`, `get_deduplication_key` is required. The callable is
|
|
115
|
+
called once per publish and must return a `str` that uniquely represents the
|
|
116
|
+
deduplication scope for that message. Returning `None` or `""` raises
|
|
117
|
+
`ConfigurationError` at publish time; returning a non-`str` value raises
|
|
118
|
+
`TypeError`.
|
|
111
119
|
|
|
112
120
|
Use stable, high-cardinality keys that include any tenant or account boundary
|
|
113
121
|
needed by your system:
|
|
@@ -525,9 +533,9 @@ avoids the parent-construct hazard entirely.
|
|
|
525
533
|
### Redis memory sizing for deduplication and replay metadata
|
|
526
534
|
|
|
527
535
|
When deduplication is enabled, each distinct dedup key creates one Redis string
|
|
528
|
-
for `message_deduplication_log_ttl_seconds` (default: 3600 seconds). The
|
|
529
|
-
|
|
530
|
-
|
|
536
|
+
for `message_deduplication_log_ttl_seconds` (default: 3600 seconds). The dedup
|
|
537
|
+
key is whatever your `get_deduplication_key` callable returns, so choose a
|
|
538
|
+
short, stable logical ID and size Redis for:
|
|
531
539
|
|
|
532
540
|
```text
|
|
533
541
|
peak_unique_publish_rate_per_second
|
|
@@ -732,6 +740,52 @@ For a full analysis, see [docs/production-readiness.md](docs/production-readines
|
|
|
732
740
|
|
|
733
741
|
## Upgrading
|
|
734
742
|
|
|
743
|
+
### v7 to v8 migration
|
|
744
|
+
|
|
745
|
+
v8.0.0 removes implicit dedup key generation. Deduplication is opt-in and
|
|
746
|
+
`deduplication=True` now requires `get_deduplication_key`. If you were relying
|
|
747
|
+
on v7's automatic content hash, provide the equivalent callable explicitly.
|
|
748
|
+
|
|
749
|
+
Before:
|
|
750
|
+
|
|
751
|
+
```python
|
|
752
|
+
queue = RedisMessageQueue(
|
|
753
|
+
"orders",
|
|
754
|
+
client=client,
|
|
755
|
+
deduplication=True,
|
|
756
|
+
)
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
After, prefer a stable logical ID:
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
queue = RedisMessageQueue(
|
|
763
|
+
"orders",
|
|
764
|
+
client=client,
|
|
765
|
+
deduplication=True,
|
|
766
|
+
get_deduplication_key=lambda msg: msg["order_id"],
|
|
767
|
+
)
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
To preserve v7 content-hash behavior mechanically:
|
|
771
|
+
|
|
772
|
+
```python
|
|
773
|
+
import hashlib
|
|
774
|
+
import json
|
|
775
|
+
|
|
776
|
+
queue = RedisMessageQueue(
|
|
777
|
+
"orders",
|
|
778
|
+
client=client,
|
|
779
|
+
deduplication=True,
|
|
780
|
+
get_deduplication_key=lambda msg: hashlib.sha256(
|
|
781
|
+
json.dumps(msg, sort_keys=True).encode()
|
|
782
|
+
).hexdigest(),
|
|
783
|
+
)
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
If you do not need publish-side deduplication, omit `deduplication` or set
|
|
787
|
+
`deduplication=False`.
|
|
788
|
+
|
|
735
789
|
### v6 to v7 migration
|
|
736
790
|
|
|
737
791
|
v7.0.0 has four breaking changes to check during upgrade.
|
|
@@ -819,7 +873,7 @@ Users on redis-py 7.x and earlier are unaffected. If you installed a redis-py
|
|
|
819
873
|
- **Do not toggle visibility timeout in either direction with messages in processing.** Messages claimed by non-VT consumers have no lease metadata, so VT-enabled consumers cannot reclaim them. Disabling VT later orphans existing lease deadline, lease token, and delivery count metadata and removes crash recovery for those in-flight messages. Drain the processing queue first.
|
|
820
874
|
- **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.
|
|
821
875
|
- **Changing `max_delivery_count` from a number to `None` leaves delivery metadata behind.** The delivery count hash continues to exist but is no longer consulted. Use this only after draining or after planning manual cleanup of the delivery-count hash.
|
|
822
|
-
- **Changing `get_deduplication_key` changes the dedup keyspace.** Existing dedup records become inert for the duration of their TTL. Drain the queue or clear the old deduplication keys before switching
|
|
876
|
+
- **Changing `get_deduplication_key` changes the dedup keyspace.** Existing dedup records become inert for the duration of their TTL. Drain the queue or clear the old deduplication keys before switching key functions.
|
|
823
877
|
- **Disabling `deduplication` has a retention-window overlap.** Existing dedup records remain in Redis until their TTL expires, but new publishes bypass them. Republishes that would have been suppressed under the old setting can enqueue during that window.
|
|
824
878
|
- **Disabling `enable_failed_queue` stops recording handler failures.** Existing failed entries remain in Redis, but new failures are removed from `processing` without being appended to the failed queue. If `max_delivery_count=None` is also set, repeated handler failures can be dropped with no DLQ or failed-queue record; see [Dead-letter queue](#dead-letter-queue).
|
|
825
879
|
- **Lowering `max_completed_length` or `max_failed_length` trims existing history.** The next completed or failed move calls `LTRIM`, so changing `None` to `N` or lowering `N` can immediately reduce historical entries to the new cap.
|
|
@@ -32,6 +32,11 @@ DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS = 1.0
|
|
|
32
32
|
INTERRUPTIBLE_RETRY_SLEEP_POLL_SECONDS = 0.05
|
|
33
33
|
PENDING_OVERLOAD_LUA_SENTINEL = -1
|
|
34
34
|
PENDING_OVERLOAD_POLICIES = ("raise", "drop_oldest", "block")
|
|
35
|
+
DEDUPLICATION_REQUIRES_KEY_MESSAGE = (
|
|
36
|
+
"deduplication=True requires get_deduplication_key (callable returning a non-empty str). "
|
|
37
|
+
"Pass a callable like `lambda msg: msg['id']` (recommended: a stable logical ID), "
|
|
38
|
+
"or set deduplication=False."
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def is_redis_retryable_exception(exception):
|
|
@@ -301,6 +306,15 @@ def validate_gateway_parameters(
|
|
|
301
306
|
)
|
|
302
307
|
|
|
303
308
|
|
|
309
|
+
def validate_dedup_configuration(
|
|
310
|
+
*,
|
|
311
|
+
deduplication: bool,
|
|
312
|
+
get_deduplication_key: object,
|
|
313
|
+
) -> None:
|
|
314
|
+
if deduplication and get_deduplication_key is None:
|
|
315
|
+
raise ConfigurationError(DEDUPLICATION_REQUIRES_KEY_MESSAGE)
|
|
316
|
+
|
|
317
|
+
|
|
304
318
|
def validate_pending_backpressure_parameters(
|
|
305
319
|
max_pending_length: int | None,
|
|
306
320
|
pending_overload_policy: str,
|
|
@@ -15,6 +15,7 @@ import redis.exceptions
|
|
|
15
15
|
from redis_message_queue._callable_utils import is_async_callable
|
|
16
16
|
from redis_message_queue._config import (
|
|
17
17
|
DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS,
|
|
18
|
+
validate_dedup_configuration,
|
|
18
19
|
validate_pending_backpressure_parameters,
|
|
19
20
|
)
|
|
20
21
|
from redis_message_queue._event import EventOperation, EventOutcome, QueueEvent
|
|
@@ -232,16 +233,6 @@ def _derive_dead_letter_queue(name: str, key_separator: str) -> str:
|
|
|
232
233
|
return f"{name}{key_separator}{_AUTO_DEAD_LETTER_QUEUE_SUFFIX}"
|
|
233
234
|
|
|
234
235
|
|
|
235
|
-
def _canonical_bytes(message: str | dict) -> bytes:
|
|
236
|
-
if isinstance(message, dict):
|
|
237
|
-
return json.dumps(message, sort_keys=True, allow_nan=False).encode("utf-8")
|
|
238
|
-
return message.encode("utf-8")
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _default_get_deduplication_key(message: str | dict) -> str:
|
|
242
|
-
return hashlib.sha256(_canonical_bytes(message)).hexdigest()
|
|
243
|
-
|
|
244
|
-
|
|
245
236
|
def _bind_dead_letter_gateway_to_queue(gateway: AbstractRedisGateway, queue_pending_key: str) -> None:
|
|
246
237
|
"""Reject reuse of a dead-letter-enabled gateway across different queues.
|
|
247
238
|
|
|
@@ -461,7 +452,7 @@ class RedisMessageQueue:
|
|
|
461
452
|
*,
|
|
462
453
|
gateway: Optional[AbstractRedisGateway] = None,
|
|
463
454
|
client: Optional[redis.asyncio.Redis] = None,
|
|
464
|
-
deduplication: bool =
|
|
455
|
+
deduplication: bool = False,
|
|
465
456
|
enable_completed_queue: bool = False,
|
|
466
457
|
enable_failed_queue: bool = False,
|
|
467
458
|
visibility_timeout_seconds: int | None = _DEFAULT_VISIBILITY_TIMEOUT_SECONDS,
|
|
@@ -473,7 +464,7 @@ class RedisMessageQueue:
|
|
|
473
464
|
pending_overload_policy: Literal["raise", "drop_oldest", "block"] = "raise",
|
|
474
465
|
pending_overload_block_timeout_seconds: float = DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS,
|
|
475
466
|
key_separator: str = "::",
|
|
476
|
-
get_deduplication_key: Optional[Callable[[str | dict], str]] =
|
|
467
|
+
get_deduplication_key: Optional[Callable[[str | dict], str]] = None,
|
|
477
468
|
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
478
469
|
on_heartbeat_failure: Callable[[], Awaitable[None] | None] | None = None,
|
|
479
470
|
on_event: Callable[[QueueEvent], Awaitable[None]] | None = None,
|
|
@@ -489,10 +480,9 @@ class RedisMessageQueue:
|
|
|
489
480
|
auto-derived dead-letter queue. Set it to ``None`` for unlimited
|
|
490
481
|
redelivery.
|
|
491
482
|
|
|
492
|
-
``get_deduplication_key``
|
|
493
|
-
|
|
494
|
-
the deduplication
|
|
495
|
-
define a custom keyspace.
|
|
483
|
+
``deduplication=True`` requires ``get_deduplication_key`` to be a
|
|
484
|
+
callable that returns a non-empty string. Use a stable logical ID for
|
|
485
|
+
the deduplication keyspace.
|
|
496
486
|
|
|
497
487
|
``max_pending_length`` defaults to ``None`` (unbounded). Set it to a
|
|
498
488
|
positive integer to cap pending-list depth during publish.
|
|
@@ -575,15 +565,17 @@ class RedisMessageQueue:
|
|
|
575
565
|
raise ConfigurationError(
|
|
576
566
|
f"'visibility_timeout_seconds' must be positive when provided, got {visibility_timeout_seconds}"
|
|
577
567
|
)
|
|
578
|
-
get_deduplication_key_was_configured =
|
|
579
|
-
get_deduplication_key is not None and get_deduplication_key is not _default_get_deduplication_key
|
|
580
|
-
)
|
|
568
|
+
get_deduplication_key_was_configured = get_deduplication_key is not None
|
|
581
569
|
if get_deduplication_key is not None and not callable(get_deduplication_key):
|
|
582
570
|
raise TypeError(
|
|
583
571
|
f"'get_deduplication_key' must be callable, got {type(get_deduplication_key).__name__}."
|
|
584
572
|
" Expected a function that takes the message (str | dict) and returns a str (or an awaitable thereof)."
|
|
585
573
|
" Example: get_deduplication_key=lambda msg: msg['user_id']"
|
|
586
574
|
)
|
|
575
|
+
validate_dedup_configuration(
|
|
576
|
+
deduplication=deduplication,
|
|
577
|
+
get_deduplication_key=get_deduplication_key,
|
|
578
|
+
)
|
|
587
579
|
validate_pending_backpressure_parameters(
|
|
588
580
|
max_pending_length,
|
|
589
581
|
pending_overload_policy,
|
|
@@ -812,13 +804,10 @@ class RedisMessageQueue:
|
|
|
812
804
|
await self._emit_event("publish", "success", duration_ms=_duration_ms(started_at))
|
|
813
805
|
return True
|
|
814
806
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
dedup_key = validate_callable_deduplication_key(dedup_key, message)
|
|
820
|
-
else:
|
|
821
|
-
dedup_key = message_str
|
|
807
|
+
dedup_key = self._get_deduplication_key(message)
|
|
808
|
+
if inspect.isawaitable(dedup_key):
|
|
809
|
+
dedup_key = await dedup_key
|
|
810
|
+
dedup_key = validate_callable_deduplication_key(dedup_key, message)
|
|
822
811
|
dedup_key = self.key.deduplication(dedup_key)
|
|
823
812
|
|
|
824
813
|
try:
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -16,6 +16,7 @@ from redis_message_queue._abstract_redis_gateway import AbstractRedisGateway
|
|
|
16
16
|
from redis_message_queue._callable_utils import is_async_callable
|
|
17
17
|
from redis_message_queue._config import (
|
|
18
18
|
DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS,
|
|
19
|
+
validate_dedup_configuration,
|
|
19
20
|
validate_pending_backpressure_parameters,
|
|
20
21
|
)
|
|
21
22
|
from redis_message_queue._event import EventOperation, EventOutcome, QueueEvent
|
|
@@ -166,16 +167,6 @@ def _derive_dead_letter_queue(name: str, key_separator: str) -> str:
|
|
|
166
167
|
return f"{name}{key_separator}{_AUTO_DEAD_LETTER_QUEUE_SUFFIX}"
|
|
167
168
|
|
|
168
169
|
|
|
169
|
-
def _canonical_bytes(message: str | dict) -> bytes:
|
|
170
|
-
if isinstance(message, dict):
|
|
171
|
-
return json.dumps(message, sort_keys=True, allow_nan=False).encode("utf-8")
|
|
172
|
-
return message.encode("utf-8")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _default_get_deduplication_key(message: str | dict) -> str:
|
|
176
|
-
return hashlib.sha256(_canonical_bytes(message)).hexdigest()
|
|
177
|
-
|
|
178
|
-
|
|
179
170
|
def _bind_dead_letter_gateway_to_queue(gateway: AbstractRedisGateway, queue_pending_key: str) -> None:
|
|
180
171
|
"""Reject reuse of a dead-letter-enabled gateway across different queues.
|
|
181
172
|
|
|
@@ -402,7 +393,7 @@ class RedisMessageQueue:
|
|
|
402
393
|
*,
|
|
403
394
|
gateway: Optional[AbstractRedisGateway] = None,
|
|
404
395
|
client: Optional[redis.Redis] = None,
|
|
405
|
-
deduplication: bool =
|
|
396
|
+
deduplication: bool = False,
|
|
406
397
|
enable_completed_queue: bool = False,
|
|
407
398
|
enable_failed_queue: bool = False,
|
|
408
399
|
visibility_timeout_seconds: int | None = _DEFAULT_VISIBILITY_TIMEOUT_SECONDS,
|
|
@@ -414,7 +405,7 @@ class RedisMessageQueue:
|
|
|
414
405
|
pending_overload_policy: Literal["raise", "drop_oldest", "block"] = "raise",
|
|
415
406
|
pending_overload_block_timeout_seconds: float = DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS,
|
|
416
407
|
key_separator: str = "::",
|
|
417
|
-
get_deduplication_key: Optional[Callable[[str | dict], str]] =
|
|
408
|
+
get_deduplication_key: Optional[Callable[[str | dict], str]] = None,
|
|
418
409
|
interrupt: BaseGracefulInterruptHandler | None = None,
|
|
419
410
|
on_heartbeat_failure: Callable[[], None] | None = None,
|
|
420
411
|
on_event: Callable[[QueueEvent], None] | None = None,
|
|
@@ -430,10 +421,9 @@ class RedisMessageQueue:
|
|
|
430
421
|
auto-derived dead-letter queue. Set it to ``None`` for unlimited
|
|
431
422
|
redelivery.
|
|
432
423
|
|
|
433
|
-
``get_deduplication_key``
|
|
434
|
-
|
|
435
|
-
the deduplication
|
|
436
|
-
keyspace.
|
|
424
|
+
``deduplication=True`` requires ``get_deduplication_key`` to be a
|
|
425
|
+
callable that returns a non-empty string. Use a stable logical ID for
|
|
426
|
+
the deduplication keyspace.
|
|
437
427
|
|
|
438
428
|
``max_pending_length`` defaults to ``None`` (unbounded). Set it to a
|
|
439
429
|
positive integer to cap pending-list depth during publish.
|
|
@@ -515,9 +505,7 @@ class RedisMessageQueue:
|
|
|
515
505
|
raise ConfigurationError(
|
|
516
506
|
f"'visibility_timeout_seconds' must be positive when provided, got {visibility_timeout_seconds}"
|
|
517
507
|
)
|
|
518
|
-
get_deduplication_key_was_configured =
|
|
519
|
-
get_deduplication_key is not None and get_deduplication_key is not _default_get_deduplication_key
|
|
520
|
-
)
|
|
508
|
+
get_deduplication_key_was_configured = get_deduplication_key is not None
|
|
521
509
|
if get_deduplication_key is not None and not callable(get_deduplication_key):
|
|
522
510
|
raise TypeError(
|
|
523
511
|
f"'get_deduplication_key' must be callable, got {type(get_deduplication_key).__name__}."
|
|
@@ -529,6 +517,10 @@ class RedisMessageQueue:
|
|
|
529
517
|
"'get_deduplication_key' is an async callable; "
|
|
530
518
|
"use the async RedisMessageQueue from redis_message_queue.asyncio instead"
|
|
531
519
|
)
|
|
520
|
+
validate_dedup_configuration(
|
|
521
|
+
deduplication=deduplication,
|
|
522
|
+
get_deduplication_key=get_deduplication_key,
|
|
523
|
+
)
|
|
532
524
|
validate_pending_backpressure_parameters(
|
|
533
525
|
max_pending_length,
|
|
534
526
|
pending_overload_policy,
|
|
@@ -766,22 +758,18 @@ class RedisMessageQueue:
|
|
|
766
758
|
self._emit_event("publish", "success", duration_ms=_duration_ms(started_at))
|
|
767
759
|
return True
|
|
768
760
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if is_coroutine:
|
|
775
|
-
raise TypeError(
|
|
776
|
-
"'get_deduplication_key' returned a coroutine; "
|
|
777
|
-
"use the async RedisMessageQueue for async callables"
|
|
778
|
-
)
|
|
761
|
+
dedup_key = self._get_deduplication_key(message)
|
|
762
|
+
if inspect.isawaitable(dedup_key):
|
|
763
|
+
is_coroutine = inspect.iscoroutine(dedup_key)
|
|
764
|
+
_close_or_cancel_awaitable(dedup_key)
|
|
765
|
+
if is_coroutine:
|
|
779
766
|
raise TypeError(
|
|
780
|
-
"'get_deduplication_key' returned
|
|
767
|
+
"'get_deduplication_key' returned a coroutine; use the async RedisMessageQueue for async callables"
|
|
781
768
|
)
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
769
|
+
raise TypeError(
|
|
770
|
+
"'get_deduplication_key' returned an awaitable; use the async RedisMessageQueue for async callables"
|
|
771
|
+
)
|
|
772
|
+
dedup_key = validate_callable_deduplication_key(dedup_key, message)
|
|
785
773
|
dedup_key = self.key.deduplication(dedup_key)
|
|
786
774
|
|
|
787
775
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_redis_gateway.py
RENAMED
|
File without changes
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-7.0.1 → redis_message_queue-8.0.0}/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
|