redis-message-queue 8.2.10__tar.gz → 8.3.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-8.2.10 → redis_message_queue-8.3.0}/PKG-INFO +44 -13
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/README.md +43 -12
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/pyproject.toml +2 -2
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_config.py +49 -25
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_redis_gateway.py +10 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/asyncio/_redis_gateway.py +10 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/asyncio/redis_message_queue.py +34 -6
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/redis_message_queue.py +32 -5
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/.gitignore +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/LICENSE +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_event.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_exceptions.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_payload_limits.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/interrupt_handler/_event_driven.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-8.2.10 → redis_message_queue-8.3.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: 8.
|
|
3
|
+
Version: 8.3.0
|
|
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,10 +34,10 @@ 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.
|
|
37
|
+
pip install "redis-message-queue>=8.3.0,<9.0.0"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Requires Redis server >= 6.2.
|
|
40
|
+
Requires Python >= 3.12 and Redis server >= 6.2.
|
|
41
41
|
|
|
42
42
|
## Quickstart
|
|
43
43
|
|
|
@@ -764,22 +764,35 @@ work. Package logs remain diagnostic; use `on_event` rather than log parsing
|
|
|
764
764
|
for metrics.
|
|
765
765
|
|
|
766
766
|
```python
|
|
767
|
-
from opentelemetry import trace
|
|
768
|
-
from prometheus_client import Counter
|
|
769
767
|
from redis_message_queue import QueueEvent, RedisMessageQueue
|
|
770
768
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
769
|
+
try:
|
|
770
|
+
from opentelemetry import trace
|
|
771
|
+
except ImportError:
|
|
772
|
+
trace = None
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
from prometheus_client import Counter
|
|
776
|
+
except ImportError:
|
|
777
|
+
Counter = None
|
|
778
|
+
|
|
779
|
+
events_total = (
|
|
780
|
+
Counter(
|
|
781
|
+
"rmq_events_total",
|
|
782
|
+
"redis-message-queue lifecycle events",
|
|
783
|
+
["queue", "operation", "outcome", "exception_type"],
|
|
784
|
+
)
|
|
785
|
+
if Counter is not None
|
|
786
|
+
else None
|
|
775
787
|
)
|
|
776
788
|
SPAN_SINK_TRUSTED = False
|
|
777
789
|
|
|
778
790
|
def observe(event: QueueEvent) -> None:
|
|
779
|
-
events_total
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
791
|
+
if events_total is not None:
|
|
792
|
+
events_total.labels(
|
|
793
|
+
event.queue, event.operation, event.outcome, event.exception_type or ""
|
|
794
|
+
).inc()
|
|
795
|
+
if event.error is not None and SPAN_SINK_TRUSTED and trace is not None:
|
|
783
796
|
trace.get_current_span().record_exception(event.error)
|
|
784
797
|
|
|
785
798
|
queue = RedisMessageQueue("jobs", client=client, on_event=observe)
|
|
@@ -830,6 +843,13 @@ Callbacks fire inline:
|
|
|
830
843
|
copies the context present when the heartbeat was started, so contextvars and
|
|
831
844
|
OpenTelemetry spans bound at handler entry are visible.
|
|
832
845
|
|
|
846
|
+
> **Warning:** Because callbacks fire inline and may run while an internal
|
|
847
|
+
> publish/drain lock is held, an `on_event` callback must not call back into the
|
|
848
|
+
> same queue instance's `publish()`, `drain()`, `close()` (sync), or `aclose()`
|
|
849
|
+
> (async). Those locks are non-reentrant, so re-entering deadlocks and wedges
|
|
850
|
+
> the caller permanently. Re-entering a *different* queue instance, or scheduling
|
|
851
|
+
> the follow-up work outside the callback, is safe.
|
|
852
|
+
|
|
833
853
|
#### Event timing vs. Redis commit
|
|
834
854
|
|
|
835
855
|
Most events are post-commit, emitted after the Redis command or Lua script
|
|
@@ -889,6 +909,17 @@ The following operations have no `on_event` surface by design:
|
|
|
889
909
|
ack/remove, move-to-completed/failed, and lease renewal collapse into the
|
|
890
910
|
terminal operation's failure event. There is no per-attempt event for those
|
|
891
911
|
paths.
|
|
912
|
+
- **Claim cache-replay after a lost reply:** a visibility-timeout claim can
|
|
913
|
+
commit server-side — dead-lettering a poison message (`dlq`) or reclaiming an
|
|
914
|
+
expired lease (`claim_reclaim`) before claiming the next live message — and
|
|
915
|
+
then lose its reply (for example, a dropped connection). The claim loop
|
|
916
|
+
retries the same claim ID and hits the `claim_result` cache-replay, which
|
|
917
|
+
re-asserts the lease and returns the stored claim but does not re-run those
|
|
918
|
+
side effects, so their `claim_reclaim` / `dlq` event payloads are not
|
|
919
|
+
re-emitted. Queue state stays correct (the poison message stays
|
|
920
|
+
dead-lettered, the live message is claimed exactly once); only telemetry for
|
|
921
|
+
the lost-reply attempt is dropped. Reconcile poison-message alerting against
|
|
922
|
+
`LLEN {name}::dlq` rather than the `on_event` stream alone.
|
|
892
923
|
|
|
893
924
|
The public exception hierarchy is rooted at `RedisMessageQueueError`. The
|
|
894
925
|
current exported queue-owned exception classes are:
|
|
@@ -11,10 +11,10 @@
|
|
|
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.
|
|
14
|
+
pip install "redis-message-queue>=8.3.0,<9.0.0"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Requires Redis server >= 6.2.
|
|
17
|
+
Requires Python >= 3.12 and Redis server >= 6.2.
|
|
18
18
|
|
|
19
19
|
## Quickstart
|
|
20
20
|
|
|
@@ -741,22 +741,35 @@ work. Package logs remain diagnostic; use `on_event` rather than log parsing
|
|
|
741
741
|
for metrics.
|
|
742
742
|
|
|
743
743
|
```python
|
|
744
|
-
from opentelemetry import trace
|
|
745
|
-
from prometheus_client import Counter
|
|
746
744
|
from redis_message_queue import QueueEvent, RedisMessageQueue
|
|
747
745
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
746
|
+
try:
|
|
747
|
+
from opentelemetry import trace
|
|
748
|
+
except ImportError:
|
|
749
|
+
trace = None
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
from prometheus_client import Counter
|
|
753
|
+
except ImportError:
|
|
754
|
+
Counter = None
|
|
755
|
+
|
|
756
|
+
events_total = (
|
|
757
|
+
Counter(
|
|
758
|
+
"rmq_events_total",
|
|
759
|
+
"redis-message-queue lifecycle events",
|
|
760
|
+
["queue", "operation", "outcome", "exception_type"],
|
|
761
|
+
)
|
|
762
|
+
if Counter is not None
|
|
763
|
+
else None
|
|
752
764
|
)
|
|
753
765
|
SPAN_SINK_TRUSTED = False
|
|
754
766
|
|
|
755
767
|
def observe(event: QueueEvent) -> None:
|
|
756
|
-
events_total
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
768
|
+
if events_total is not None:
|
|
769
|
+
events_total.labels(
|
|
770
|
+
event.queue, event.operation, event.outcome, event.exception_type or ""
|
|
771
|
+
).inc()
|
|
772
|
+
if event.error is not None and SPAN_SINK_TRUSTED and trace is not None:
|
|
760
773
|
trace.get_current_span().record_exception(event.error)
|
|
761
774
|
|
|
762
775
|
queue = RedisMessageQueue("jobs", client=client, on_event=observe)
|
|
@@ -807,6 +820,13 @@ Callbacks fire inline:
|
|
|
807
820
|
copies the context present when the heartbeat was started, so contextvars and
|
|
808
821
|
OpenTelemetry spans bound at handler entry are visible.
|
|
809
822
|
|
|
823
|
+
> **Warning:** Because callbacks fire inline and may run while an internal
|
|
824
|
+
> publish/drain lock is held, an `on_event` callback must not call back into the
|
|
825
|
+
> same queue instance's `publish()`, `drain()`, `close()` (sync), or `aclose()`
|
|
826
|
+
> (async). Those locks are non-reentrant, so re-entering deadlocks and wedges
|
|
827
|
+
> the caller permanently. Re-entering a *different* queue instance, or scheduling
|
|
828
|
+
> the follow-up work outside the callback, is safe.
|
|
829
|
+
|
|
810
830
|
#### Event timing vs. Redis commit
|
|
811
831
|
|
|
812
832
|
Most events are post-commit, emitted after the Redis command or Lua script
|
|
@@ -866,6 +886,17 @@ The following operations have no `on_event` surface by design:
|
|
|
866
886
|
ack/remove, move-to-completed/failed, and lease renewal collapse into the
|
|
867
887
|
terminal operation's failure event. There is no per-attempt event for those
|
|
868
888
|
paths.
|
|
889
|
+
- **Claim cache-replay after a lost reply:** a visibility-timeout claim can
|
|
890
|
+
commit server-side — dead-lettering a poison message (`dlq`) or reclaiming an
|
|
891
|
+
expired lease (`claim_reclaim`) before claiming the next live message — and
|
|
892
|
+
then lose its reply (for example, a dropped connection). The claim loop
|
|
893
|
+
retries the same claim ID and hits the `claim_result` cache-replay, which
|
|
894
|
+
re-asserts the lease and returns the stored claim but does not re-run those
|
|
895
|
+
side effects, so their `claim_reclaim` / `dlq` event payloads are not
|
|
896
|
+
re-emitted. Queue state stays correct (the poison message stays
|
|
897
|
+
dead-lettered, the live message is claimed exactly once); only telemetry for
|
|
898
|
+
the lost-reply attempt is dropped. Reconcile poison-message alerting against
|
|
899
|
+
`LLEN {name}::dlq` rather than the `on_event` stream alone.
|
|
869
900
|
|
|
870
901
|
The public exception hierarchy is rooted at `RedisMessageQueueError`. The
|
|
871
902
|
current exported queue-owned exception classes are:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "redis-message-queue"
|
|
3
|
-
version = "8.
|
|
3
|
+
version = "8.3.0"
|
|
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.
|
|
51
|
+
current_version = "8.3.0"
|
|
52
52
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
53
53
|
serialize = ["{major}.{minor}.{patch}"]
|
|
54
54
|
search = "{current_version}"
|
|
@@ -358,7 +358,13 @@ def validate_pending_backpressure_parameters(
|
|
|
358
358
|
raise ConfigurationError(
|
|
359
359
|
"drop_oldest requires max_pending_length to be set. "
|
|
360
360
|
"Use a positive max_pending_length to define what can be dropped, or use "
|
|
361
|
-
"pending_overload_policy='raise'
|
|
361
|
+
"pending_overload_policy='raise' for an unbounded queue."
|
|
362
|
+
)
|
|
363
|
+
if pending_overload_policy == "block" and max_pending_length is None:
|
|
364
|
+
raise ConfigurationError(
|
|
365
|
+
"block requires max_pending_length to be set. "
|
|
366
|
+
"Use a positive max_pending_length to define the threshold to block on, or use "
|
|
367
|
+
"pending_overload_policy='raise' for an unbounded queue."
|
|
362
368
|
)
|
|
363
369
|
if pending_overload_policy == "drop_oldest" and (deduplication or get_deduplication_key_configured):
|
|
364
370
|
raise ConfigurationError(
|
|
@@ -857,35 +863,53 @@ end
|
|
|
857
863
|
-- Cap at 100 to bound Lua execution time (Redis blocks during scripts).
|
|
858
864
|
-- With a single consumer polling at default interval, 1000 expired leases drain in ~2.5s.
|
|
859
865
|
local expired = redis.call('ZRANGEBYSCORE', KEYS[3], '-inf', now_ms, 'LIMIT', 0, 100)
|
|
860
|
-
local to_requeue = {}
|
|
861
866
|
local reclaimed_events = {}
|
|
862
867
|
for i = #expired, 1, -1 do
|
|
863
|
-
local
|
|
864
|
-
redis.call('
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
868
|
+
local stored = expired[i]
|
|
869
|
+
local expired_lease_token = redis.call('HGET', KEYS[4], stored)
|
|
870
|
+
|
|
871
|
+
-- Durable-before-destructive (mirror RETURN_MESSAGE_TO_PENDING): requeue to
|
|
872
|
+
-- pending BEFORE removing from processing or deleting lease metadata. If this
|
|
873
|
+
-- write fails, the message remains in processing with its metadata intact for
|
|
874
|
+
-- a future reclaim attempt.
|
|
875
|
+
redis.call('RPUSH', KEYS[1], stored)
|
|
876
|
+
if redis.call('LREM', KEYS[2], 1, stored) == 1 then
|
|
877
|
+
redis.call('ZREM', KEYS[3], stored)
|
|
878
|
+
redis.call('HDEL', KEYS[4], stored)
|
|
879
|
+
if expired_lease_token then
|
|
880
|
+
local claim_result_key = redis.call('HGET', KEYS[9], expired_lease_token)
|
|
881
|
+
if claim_result_key then
|
|
882
|
+
-- Use pcall: in Redis Cluster, claim_result_key was read from KEYS[9] (claim_result_refs)
|
|
883
|
+
-- and is therefore not in the declared EVAL KEYS[] set. Cluster may reject the DEL;
|
|
884
|
+
-- TTL on the claim_result string (PX visibility_timeout_seconds) bounds the orphan.
|
|
885
|
+
redis.pcall('DEL', claim_result_key)
|
|
886
|
+
redis.call('HDEL', KEYS[9], expired_lease_token)
|
|
887
|
+
end
|
|
888
|
+
local claim_id = redis.call('HGET', KEYS[11], expired_lease_token)
|
|
889
|
+
if claim_id then
|
|
890
|
+
redis.call('HDEL', KEYS[10], claim_id)
|
|
891
|
+
redis.call('HDEL', KEYS[11], expired_lease_token)
|
|
892
|
+
end
|
|
874
893
|
end
|
|
875
|
-
local
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
894
|
+
local delivery_count = redis.call('HGET', KEYS[6], stored)
|
|
895
|
+
table.insert(reclaimed_events, {redis_message_queue_message_id(stored), tostring(delivery_count or '0')})
|
|
896
|
+
else
|
|
897
|
+
redis.call('LREM', KEYS[1], 1, stored)
|
|
898
|
+
redis.call('ZREM', KEYS[3], stored)
|
|
899
|
+
redis.call('HDEL', KEYS[4], stored)
|
|
900
|
+
if expired_lease_token then
|
|
901
|
+
local claim_result_key = redis.call('HGET', KEYS[9], expired_lease_token)
|
|
902
|
+
if claim_result_key then
|
|
903
|
+
redis.pcall('DEL', claim_result_key)
|
|
904
|
+
redis.call('HDEL', KEYS[9], expired_lease_token)
|
|
905
|
+
end
|
|
906
|
+
local claim_id = redis.call('HGET', KEYS[11], expired_lease_token)
|
|
907
|
+
if claim_id then
|
|
908
|
+
redis.call('HDEL', KEYS[10], claim_id)
|
|
909
|
+
redis.call('HDEL', KEYS[11], expired_lease_token)
|
|
910
|
+
end
|
|
879
911
|
end
|
|
880
912
|
end
|
|
881
|
-
if redis.call('LREM', KEYS[2], 1, expired[i]) == 1 then
|
|
882
|
-
table.insert(to_requeue, expired[i])
|
|
883
|
-
local delivery_count = redis.call('HGET', KEYS[6], expired[i])
|
|
884
|
-
table.insert(reclaimed_events, {redis_message_queue_message_id(expired[i]), tostring(delivery_count or '0')})
|
|
885
|
-
end
|
|
886
|
-
end
|
|
887
|
-
if #to_requeue > 0 then
|
|
888
|
-
redis.call('RPUSH', KEYS[1], unpack(to_requeue))
|
|
889
913
|
end
|
|
890
914
|
local dead_lettered_events = {}
|
|
891
915
|
local claim_store_failed_sentinel = string.char(0) .. '__rmq_claim_store_failed__'
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -402,6 +402,15 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
402
402
|
def is_redis_cluster(self) -> bool:
|
|
403
403
|
return isinstance(self._redis_client, redis.RedisCluster)
|
|
404
404
|
|
|
405
|
+
def _raise_if_drop_oldest_deduplicated_publish(self) -> None:
|
|
406
|
+
if self._pending_overload_policy == "drop_oldest":
|
|
407
|
+
raise ConfigurationError(
|
|
408
|
+
"'pending_overload_policy=drop_oldest' cannot be used with RedisGateway.publish_message "
|
|
409
|
+
"because dropped messages leave their deduplication keys in Redis, causing future publishes "
|
|
410
|
+
"of the same payload to be silently suppressed. Use add_message for non-deduplicated lossy "
|
|
411
|
+
"queues, or use 'raise' or 'block' for deduplicated publishes."
|
|
412
|
+
)
|
|
413
|
+
|
|
405
414
|
def publish_message(self, queue: str, message: str, dedup_key: str) -> bool:
|
|
406
415
|
if not isinstance(dedup_key, str):
|
|
407
416
|
raise TypeError(f"'dedup_key' must be a str, got {type(dedup_key).__name__}")
|
|
@@ -410,6 +419,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
410
419
|
"'dedup_key' must be a non-empty string; "
|
|
411
420
|
"an empty key would create a bare-prefix Redis marker that silently suppresses unrelated messages"
|
|
412
421
|
)
|
|
422
|
+
self._raise_if_drop_oldest_deduplicated_publish()
|
|
413
423
|
stored_message = encode_stored_message(message)
|
|
414
424
|
message_id = extract_stored_message_id(stored_message)
|
|
415
425
|
operation_id = uuid.uuid4().hex
|
|
@@ -381,6 +381,15 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
381
381
|
def is_redis_cluster(self) -> bool:
|
|
382
382
|
return isinstance(self._redis_client, redis.asyncio.RedisCluster)
|
|
383
383
|
|
|
384
|
+
def _raise_if_drop_oldest_deduplicated_publish(self) -> None:
|
|
385
|
+
if self._pending_overload_policy == "drop_oldest":
|
|
386
|
+
raise ConfigurationError(
|
|
387
|
+
"'pending_overload_policy=drop_oldest' cannot be used with RedisGateway.publish_message "
|
|
388
|
+
"because dropped messages leave their deduplication keys in Redis, causing future publishes "
|
|
389
|
+
"of the same payload to be silently suppressed. Use add_message for non-deduplicated lossy "
|
|
390
|
+
"queues, or use 'raise' or 'block' for deduplicated publishes."
|
|
391
|
+
)
|
|
392
|
+
|
|
384
393
|
async def publish_message(self, queue: str, message: str, dedup_key: str) -> bool:
|
|
385
394
|
if not isinstance(dedup_key, str):
|
|
386
395
|
raise TypeError(f"'dedup_key' must be a str, got {type(dedup_key).__name__}")
|
|
@@ -389,6 +398,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
389
398
|
"'dedup_key' must be a non-empty string; "
|
|
390
399
|
"an empty key would create a bare-prefix Redis marker that silently suppresses unrelated messages"
|
|
391
400
|
)
|
|
401
|
+
self._raise_if_drop_oldest_deduplicated_publish()
|
|
392
402
|
stored_message = encode_stored_message(message)
|
|
393
403
|
message_id = extract_stored_message_id(stored_message)
|
|
394
404
|
operation_id = uuid.uuid4().hex
|
|
@@ -667,6 +667,8 @@ class RedisMessageQueue:
|
|
|
667
667
|
``"drop_oldest"`` evicts the oldest pending message before enqueueing
|
|
668
668
|
the new one. ``"drop_oldest"`` requires ``max_pending_length`` and is
|
|
669
669
|
not compatible with deduplication or ``max_delivery_count``.
|
|
670
|
+
``"block"`` also requires ``max_pending_length`` (the threshold to
|
|
671
|
+
block on); only the default ``"raise"`` operates on an unbounded queue.
|
|
670
672
|
|
|
671
673
|
``pending_overload_block_timeout_seconds`` bounds how long ``"block"``
|
|
672
674
|
waits for capacity before raising ``QueueBackpressureError``. ``0``
|
|
@@ -679,12 +681,23 @@ class RedisMessageQueue:
|
|
|
679
681
|
``interrupt`` accepts a ``BaseGracefulInterruptHandler``; pass
|
|
680
682
|
``GracefulInterruptHandler()`` for prompt Ctrl-C / termination handling
|
|
681
683
|
in polling waits. ``on_heartbeat_failure`` is a zero-argument callable
|
|
682
|
-
or coroutine callable invoked when lease renewal fails.
|
|
683
|
-
|
|
684
|
+
or coroutine callable invoked when lease renewal fails. It runs on the
|
|
685
|
+
heartbeat's background thread (sync queue) or on the event loop (async
|
|
686
|
+
queue); in the async queue it MUST NOT block (no ``time.sleep``,
|
|
687
|
+
blocking I/O, or CPU-heavy work; use ``await``), because a blocking
|
|
688
|
+
callback freezes the event loop, delaying lease renewal for every other
|
|
689
|
+
in-flight message (whose leases can then expire and be reclaimed) and
|
|
690
|
+
stalling ``aclose()``. Keep it quick and offload slow work yourself.
|
|
691
|
+
``on_event`` is telemetry only: a callable returning an awaitable and
|
|
692
|
+
receiving
|
|
684
693
|
best-effort QueueEvent lifecycle notifications. Callback failures are
|
|
685
694
|
logged and converted to RuntimeWarning without influencing ack/nack or
|
|
686
695
|
any other message outcome. Do not use it for correctness-critical
|
|
687
|
-
callbacks or follow-up writes.
|
|
696
|
+
callbacks or follow-up writes. ``on_event`` is awaited inline in the
|
|
697
|
+
current asyncio task and may execute while an internal publish/drain
|
|
698
|
+
lock is held, so the callback must not call back into the same queue
|
|
699
|
+
instance's ``publish()``, ``drain()``, or ``aclose()``; that lock is
|
|
700
|
+
non-reentrant, so re-entering deadlocks the caller permanently.
|
|
688
701
|
"""
|
|
689
702
|
self.key = QueueKeyManager(name, key_separator=key_separator)
|
|
690
703
|
if not isinstance(deduplication, bool):
|
|
@@ -769,6 +782,18 @@ class RedisMessageQueue:
|
|
|
769
782
|
deduplication=deduplication,
|
|
770
783
|
get_deduplication_key=get_deduplication_key,
|
|
771
784
|
)
|
|
785
|
+
if gateway is not None:
|
|
786
|
+
# Before the generic validator so gateway-incompat wins over the drop_oldest runaround; non-default only.
|
|
787
|
+
if pending_overload_policy != "raise":
|
|
788
|
+
raise ConfigurationError(
|
|
789
|
+
"'pending_overload_policy' cannot be provided alongside 'gateway'."
|
|
790
|
+
" Configure publish backpressure on the gateway directly instead."
|
|
791
|
+
)
|
|
792
|
+
if pending_overload_block_timeout_seconds != DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS:
|
|
793
|
+
raise ConfigurationError(
|
|
794
|
+
"'pending_overload_block_timeout_seconds' cannot be provided alongside 'gateway'."
|
|
795
|
+
" Configure publish backpressure on the gateway directly instead."
|
|
796
|
+
)
|
|
772
797
|
validate_pending_backpressure_parameters(
|
|
773
798
|
max_pending_length,
|
|
774
799
|
pending_overload_policy,
|
|
@@ -1239,11 +1264,14 @@ class RedisMessageQueue:
|
|
|
1239
1264
|
)
|
|
1240
1265
|
|
|
1241
1266
|
lease_heartbeat = self._build_lease_heartbeat(stored_message, lease_token, message_id, lease_token_hash)
|
|
1242
|
-
if lease_heartbeat is not None:
|
|
1243
|
-
lease_heartbeat.start()
|
|
1244
1267
|
finished_without_error = False
|
|
1245
1268
|
processing_started_at = time.perf_counter()
|
|
1246
1269
|
try:
|
|
1270
|
+
# start() must be inside this try so its finally always stop()s the
|
|
1271
|
+
# heartbeat; an exception in the start()->yield window would
|
|
1272
|
+
# otherwise orphan it.
|
|
1273
|
+
if lease_heartbeat is not None:
|
|
1274
|
+
lease_heartbeat.start()
|
|
1247
1275
|
yield message # type: ignore
|
|
1248
1276
|
except BaseException as exc:
|
|
1249
1277
|
skip_cleanup = _should_skip_message_cleanup(exc)
|
|
@@ -1504,7 +1532,7 @@ class RedisMessageQueue:
|
|
|
1504
1532
|
timeout_seconds = None if timeout is None else float(timeout)
|
|
1505
1533
|
async with self._aclose_lock:
|
|
1506
1534
|
cleanup_lease_counter = getattr(self._redis, "_cleanup_drained_lease_token_counter", None)
|
|
1507
|
-
if self._aclose_result is
|
|
1535
|
+
if self._aclose_result is True:
|
|
1508
1536
|
pending_claim_ids = self._pending_claim_ids_count()
|
|
1509
1537
|
if pending_claim_ids:
|
|
1510
1538
|
self._aclose_result = None
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -629,6 +629,8 @@ class RedisMessageQueue:
|
|
|
629
629
|
``"drop_oldest"`` evicts the oldest pending message before enqueueing
|
|
630
630
|
the new one. ``"drop_oldest"`` requires ``max_pending_length`` and is
|
|
631
631
|
not compatible with deduplication or ``max_delivery_count``.
|
|
632
|
+
``"block"`` also requires ``max_pending_length`` (the threshold to
|
|
633
|
+
block on); only the default ``"raise"`` operates on an unbounded queue.
|
|
632
634
|
|
|
633
635
|
``pending_overload_block_timeout_seconds`` bounds how long ``"block"``
|
|
634
636
|
waits for capacity before raising ``QueueBackpressureError``. ``0``
|
|
@@ -641,11 +643,21 @@ class RedisMessageQueue:
|
|
|
641
643
|
``interrupt`` accepts a ``BaseGracefulInterruptHandler``; pass
|
|
642
644
|
``GracefulInterruptHandler()`` for prompt Ctrl-C / termination handling
|
|
643
645
|
in polling waits. ``on_heartbeat_failure`` is a zero-argument callable
|
|
644
|
-
invoked when lease renewal fails.
|
|
645
|
-
|
|
646
|
+
invoked when lease renewal fails. It runs on the heartbeat's background
|
|
647
|
+
thread (sync queue) or on the event loop (async queue); in the async
|
|
648
|
+
queue it MUST NOT block (no ``time.sleep``, blocking I/O, or CPU-heavy
|
|
649
|
+
work; use ``await``), because a blocking callback freezes the event
|
|
650
|
+
loop, delaying lease renewal for every other in-flight message (whose
|
|
651
|
+
leases can then expire and be reclaimed) and stalling ``aclose()``.
|
|
652
|
+
Keep it quick and offload slow work yourself. ``on_event`` is telemetry
|
|
653
|
+
only and receives best-effort QueueEvent lifecycle notifications; callback
|
|
646
654
|
failures are logged and converted to RuntimeWarning without influencing
|
|
647
655
|
ack/nack or any other message outcome. Do not use it for
|
|
648
|
-
correctness-critical callbacks or follow-up writes.
|
|
656
|
+
correctness-critical callbacks or follow-up writes. ``on_event`` runs
|
|
657
|
+
inline in the caller's thread and may execute while an internal
|
|
658
|
+
publish/drain lock is held, so the callback must not call back into the
|
|
659
|
+
same queue instance's ``publish()``, ``drain()``, or ``close()``; that
|
|
660
|
+
lock is non-reentrant, so re-entering deadlocks the caller permanently.
|
|
649
661
|
"""
|
|
650
662
|
self.key = QueueKeyManager(name, key_separator=key_separator)
|
|
651
663
|
if not isinstance(deduplication, bool):
|
|
@@ -734,6 +746,18 @@ class RedisMessageQueue:
|
|
|
734
746
|
deduplication=deduplication,
|
|
735
747
|
get_deduplication_key=get_deduplication_key,
|
|
736
748
|
)
|
|
749
|
+
if gateway is not None:
|
|
750
|
+
# Before the generic validator so gateway-incompat wins over the drop_oldest runaround; non-default only.
|
|
751
|
+
if pending_overload_policy != "raise":
|
|
752
|
+
raise ConfigurationError(
|
|
753
|
+
"'pending_overload_policy' cannot be provided alongside 'gateway'."
|
|
754
|
+
" Configure publish backpressure on the gateway directly instead."
|
|
755
|
+
)
|
|
756
|
+
if pending_overload_block_timeout_seconds != DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS:
|
|
757
|
+
raise ConfigurationError(
|
|
758
|
+
"'pending_overload_block_timeout_seconds' cannot be provided alongside 'gateway'."
|
|
759
|
+
" Configure publish backpressure on the gateway directly instead."
|
|
760
|
+
)
|
|
737
761
|
validate_pending_backpressure_parameters(
|
|
738
762
|
max_pending_length,
|
|
739
763
|
pending_overload_policy,
|
|
@@ -1209,10 +1233,13 @@ class RedisMessageQueue:
|
|
|
1209
1233
|
)
|
|
1210
1234
|
|
|
1211
1235
|
lease_heartbeat = self._build_lease_heartbeat(stored_message, lease_token, message_id, lease_token_hash)
|
|
1212
|
-
if lease_heartbeat is not None:
|
|
1213
|
-
lease_heartbeat.start()
|
|
1214
1236
|
processing_started_at = time.perf_counter()
|
|
1215
1237
|
try:
|
|
1238
|
+
# start() must be inside this try so its finally always stop()s the
|
|
1239
|
+
# heartbeat; an exception in the start()->yield window (e.g. a
|
|
1240
|
+
# signal-induced KeyboardInterrupt) would otherwise orphan it.
|
|
1241
|
+
if lease_heartbeat is not None:
|
|
1242
|
+
lease_heartbeat.start()
|
|
1216
1243
|
yield message # type: ignore
|
|
1217
1244
|
except BaseException as exc:
|
|
1218
1245
|
skip_cleanup = _should_skip_message_cleanup(exc)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_payload_limits.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.0}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.10 → redis_message_queue-8.3.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
|