redis-message-queue 8.0.0__tar.gz → 8.0.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-8.0.0 → redis_message_queue-8.0.1}/PKG-INFO +2 -2
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/README.md +1 -1
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/pyproject.toml +1 -1
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_redis_gateway.py +157 -25
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/asyncio/_redis_gateway.py +140 -24
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/asyncio/redis_message_queue.py +49 -8
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/redis_message_queue.py +49 -9
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/LICENSE +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_config.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_event.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_exceptions.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/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.0.
|
|
3
|
+
Version: 8.0.1
|
|
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)
|
|
@@ -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)
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import queue
|
|
3
4
|
import random
|
|
4
5
|
import threading
|
|
5
6
|
import time
|
|
6
7
|
import uuid
|
|
7
|
-
from typing import Callable, Optional, TypeVar
|
|
8
|
+
from typing import Callable, Optional, TypeVar, cast
|
|
8
9
|
|
|
9
10
|
import redis
|
|
10
11
|
import redis.asyncio
|
|
@@ -54,6 +55,7 @@ from redis_message_queue.interrupt_handler._interface import (
|
|
|
54
55
|
|
|
55
56
|
logger = logging.getLogger(__name__)
|
|
56
57
|
_TClaim = TypeVar("_TClaim", bound=ClaimedMessage | MessageData)
|
|
58
|
+
_TRedisCall = TypeVar("_TRedisCall")
|
|
57
59
|
|
|
58
60
|
_LEASE_DEADLINES_SUFFIX = ":lease_deadlines"
|
|
59
61
|
_LEASE_TOKENS_SUFFIX = ":lease_tokens"
|
|
@@ -71,6 +73,49 @@ _PENDING_OVERLOAD_INITIAL_BACKOFF_SECONDS = 0.010
|
|
|
71
73
|
_PENDING_OVERLOAD_MAX_BACKOFF_SECONDS = 0.500
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
class _DrainDeadlineExceeded(Exception):
|
|
77
|
+
"""Internal sentinel for drain-only pending-claim recovery deadlines."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _raise_if_drain_deadline_expired(deadline_monotonic: float | None) -> None:
|
|
81
|
+
if deadline_monotonic is not None and time.monotonic() >= deadline_monotonic:
|
|
82
|
+
raise _DrainDeadlineExceeded
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _call_with_drain_deadline(
|
|
86
|
+
call: Callable[[], _TRedisCall],
|
|
87
|
+
*,
|
|
88
|
+
deadline_monotonic: float | None,
|
|
89
|
+
) -> _TRedisCall:
|
|
90
|
+
if deadline_monotonic is None:
|
|
91
|
+
return call()
|
|
92
|
+
|
|
93
|
+
remaining_seconds = deadline_monotonic - time.monotonic()
|
|
94
|
+
if remaining_seconds <= 0:
|
|
95
|
+
raise _DrainDeadlineExceeded
|
|
96
|
+
|
|
97
|
+
result_queue: queue.Queue[tuple[bool, object]] = queue.Queue(maxsize=1)
|
|
98
|
+
|
|
99
|
+
def run_call() -> None:
|
|
100
|
+
try:
|
|
101
|
+
result = call()
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
result_queue.put_nowait((False, exc))
|
|
104
|
+
else:
|
|
105
|
+
result_queue.put_nowait((True, result))
|
|
106
|
+
|
|
107
|
+
thread = threading.Thread(target=run_call, daemon=True)
|
|
108
|
+
thread.start()
|
|
109
|
+
thread.join(timeout=remaining_seconds)
|
|
110
|
+
if thread.is_alive():
|
|
111
|
+
raise _DrainDeadlineExceeded
|
|
112
|
+
|
|
113
|
+
succeeded, value = result_queue.get_nowait()
|
|
114
|
+
if succeeded:
|
|
115
|
+
return cast(_TRedisCall, value)
|
|
116
|
+
raise cast(BaseException, value)
|
|
117
|
+
|
|
118
|
+
|
|
74
119
|
def _coerce_lua_count(value: object) -> int:
|
|
75
120
|
if isinstance(value, bytes):
|
|
76
121
|
value = value.decode("utf-8")
|
|
@@ -525,6 +570,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
525
570
|
claim_message: Callable[[str, str, str], _TClaim | None],
|
|
526
571
|
non_blocking_retry_log: str,
|
|
527
572
|
polling_retry_log: str,
|
|
573
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
528
574
|
) -> _TClaim | None:
|
|
529
575
|
while True:
|
|
530
576
|
pending_claim_id = self._acquire_pending_claim_id(to_queue)
|
|
@@ -552,7 +598,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
552
598
|
self._emit_event("claim_reclaim", "success", claim_id=pending_claim_id)
|
|
553
599
|
return recovered_claim
|
|
554
600
|
|
|
555
|
-
if self._is_interrupted():
|
|
601
|
+
if self._is_interrupted(is_interrupted):
|
|
556
602
|
return None
|
|
557
603
|
|
|
558
604
|
pending_claim_id_to_share: str | None = None
|
|
@@ -575,7 +621,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
575
621
|
error=exc,
|
|
576
622
|
)
|
|
577
623
|
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
578
|
-
if self._is_interrupted():
|
|
624
|
+
if self._is_interrupted(is_interrupted):
|
|
579
625
|
pending_claim_id_to_share = claim_id
|
|
580
626
|
return None
|
|
581
627
|
try:
|
|
@@ -607,7 +653,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
607
653
|
claim_may_need_recovery = False
|
|
608
654
|
last_retryable_exception: Exception | None = None
|
|
609
655
|
while True:
|
|
610
|
-
if self._is_interrupted():
|
|
656
|
+
if self._is_interrupted(is_interrupted):
|
|
611
657
|
if claim_may_need_recovery:
|
|
612
658
|
pending_claim_id_to_share = claim_id
|
|
613
659
|
return None
|
|
@@ -640,7 +686,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
640
686
|
remaining = deadline - time.monotonic()
|
|
641
687
|
if remaining <= 0:
|
|
642
688
|
if last_retryable_exception is not None:
|
|
643
|
-
if self._is_interrupted():
|
|
689
|
+
if self._is_interrupted(is_interrupted):
|
|
644
690
|
if claim_may_need_recovery:
|
|
645
691
|
pending_claim_id_to_share = claim_id
|
|
646
692
|
return None
|
|
@@ -675,11 +721,36 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
675
721
|
def wait_for_message_and_move(self, from_queue: str, to_queue: str) -> ClaimedMessage | MessageData | None:
|
|
676
722
|
if self._is_interrupted():
|
|
677
723
|
return None
|
|
724
|
+
return self._wait_for_message_and_move_interruptible(from_queue, to_queue)
|
|
725
|
+
|
|
726
|
+
def _wait_for_message_and_move_interruptible(
|
|
727
|
+
self,
|
|
728
|
+
from_queue: str,
|
|
729
|
+
to_queue: str,
|
|
730
|
+
*,
|
|
731
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
732
|
+
) -> ClaimedMessage | MessageData | None:
|
|
733
|
+
if self._is_interrupted(is_interrupted):
|
|
734
|
+
return None
|
|
678
735
|
if self._message_visibility_timeout_seconds is not None:
|
|
679
|
-
return self._wait_for_message_with_visibility_timeout(
|
|
680
|
-
|
|
736
|
+
return self._wait_for_message_with_visibility_timeout(
|
|
737
|
+
from_queue,
|
|
738
|
+
to_queue,
|
|
739
|
+
is_interrupted=is_interrupted,
|
|
740
|
+
)
|
|
741
|
+
return self._wait_for_message_without_visibility_timeout(
|
|
742
|
+
from_queue,
|
|
743
|
+
to_queue,
|
|
744
|
+
is_interrupted=is_interrupted,
|
|
745
|
+
)
|
|
681
746
|
|
|
682
|
-
def _wait_for_message_without_visibility_timeout(
|
|
747
|
+
def _wait_for_message_without_visibility_timeout(
|
|
748
|
+
self,
|
|
749
|
+
from_queue: str,
|
|
750
|
+
to_queue: str,
|
|
751
|
+
*,
|
|
752
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
753
|
+
) -> MessageData | None:
|
|
683
754
|
return self._wait_for_claim(
|
|
684
755
|
from_queue,
|
|
685
756
|
to_queue,
|
|
@@ -693,9 +764,16 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
693
764
|
"Transient error during non-visibility-timeout non-blocking claim, retrying once to recover claim: %s"
|
|
694
765
|
),
|
|
695
766
|
polling_retry_log="Transient error during non-visibility-timeout claim poll, will retry: %s",
|
|
767
|
+
is_interrupted=is_interrupted,
|
|
696
768
|
)
|
|
697
769
|
|
|
698
|
-
def _wait_for_message_with_visibility_timeout(
|
|
770
|
+
def _wait_for_message_with_visibility_timeout(
|
|
771
|
+
self,
|
|
772
|
+
from_queue: str,
|
|
773
|
+
to_queue: str,
|
|
774
|
+
*,
|
|
775
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
776
|
+
) -> ClaimedMessage | None:
|
|
699
777
|
return self._wait_for_claim(
|
|
700
778
|
from_queue,
|
|
701
779
|
to_queue,
|
|
@@ -709,6 +787,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
709
787
|
"Transient error during visibility-timeout non-blocking claim, retrying once to recover claim: %s"
|
|
710
788
|
),
|
|
711
789
|
polling_retry_log="Transient error during visibility-timeout claim poll, will retry: %s",
|
|
790
|
+
is_interrupted=is_interrupted,
|
|
712
791
|
)
|
|
713
792
|
|
|
714
793
|
def _claim_message_without_visibility_timeout(
|
|
@@ -833,16 +912,37 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
833
912
|
def _claim_result_ttl_ms(self) -> str:
|
|
834
913
|
return str(max(self._message_wait_interval_seconds, 120) * 1000)
|
|
835
914
|
|
|
836
|
-
def _delete_claim_result_key(
|
|
915
|
+
def _delete_claim_result_key(
|
|
916
|
+
self,
|
|
917
|
+
claim_result_key: str,
|
|
918
|
+
*,
|
|
919
|
+
deadline_monotonic: float | None = None,
|
|
920
|
+
) -> None:
|
|
837
921
|
try:
|
|
838
|
-
|
|
922
|
+
_call_with_drain_deadline(
|
|
923
|
+
lambda: self._redis_client.delete(claim_result_key),
|
|
924
|
+
deadline_monotonic=deadline_monotonic,
|
|
925
|
+
)
|
|
926
|
+
except _DrainDeadlineExceeded:
|
|
927
|
+
raise
|
|
839
928
|
except Exception:
|
|
840
929
|
# Claim-result keys have bounded TTLs; this cleanup is intentionally best-effort.
|
|
841
930
|
logger.warning("Failed to delete claim result key %s", claim_result_key, exc_info=True)
|
|
842
931
|
|
|
843
|
-
def _delete_claim_result_ref(
|
|
932
|
+
def _delete_claim_result_ref(
|
|
933
|
+
self,
|
|
934
|
+
claim_result_refs_key: str,
|
|
935
|
+
lease_token: str,
|
|
936
|
+
*,
|
|
937
|
+
deadline_monotonic: float | None = None,
|
|
938
|
+
) -> None:
|
|
844
939
|
try:
|
|
845
|
-
|
|
940
|
+
_call_with_drain_deadline(
|
|
941
|
+
lambda: self._redis_client.hdel(claim_result_refs_key, lease_token),
|
|
942
|
+
deadline_monotonic=deadline_monotonic,
|
|
943
|
+
)
|
|
944
|
+
except _DrainDeadlineExceeded:
|
|
945
|
+
raise
|
|
846
946
|
except Exception:
|
|
847
947
|
# Claim-result refs have bounded TTLs; this cleanup is intentionally best-effort.
|
|
848
948
|
logger.warning(
|
|
@@ -906,25 +1006,48 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
906
1006
|
self,
|
|
907
1007
|
processing_queue: str,
|
|
908
1008
|
claim_id: str,
|
|
1009
|
+
*,
|
|
1010
|
+
deadline_monotonic: float | None = None,
|
|
909
1011
|
) -> MessageData | None:
|
|
1012
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
910
1013
|
claim_result_key = self._claim_result_key(processing_queue, claim_id)
|
|
911
|
-
cached_claim =
|
|
1014
|
+
cached_claim = _call_with_drain_deadline(
|
|
1015
|
+
lambda: self._redis_client.get(claim_result_key),
|
|
1016
|
+
deadline_monotonic=deadline_monotonic,
|
|
1017
|
+
)
|
|
1018
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
912
1019
|
if cached_claim is None:
|
|
913
|
-
cached_claim =
|
|
1020
|
+
cached_claim = _call_with_drain_deadline(
|
|
1021
|
+
lambda: self._redis_client.hget(self._claim_result_ids_key(processing_queue), claim_id),
|
|
1022
|
+
deadline_monotonic=deadline_monotonic,
|
|
1023
|
+
)
|
|
1024
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
914
1025
|
if cached_claim is None:
|
|
915
1026
|
return None
|
|
916
|
-
self._delete_claim_result_key(claim_result_key)
|
|
1027
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
1028
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
917
1029
|
return cached_claim
|
|
918
1030
|
|
|
919
1031
|
def _recover_pending_visibility_timeout_claim(
|
|
920
1032
|
self,
|
|
921
1033
|
processing_queue: str,
|
|
922
1034
|
claim_id: str,
|
|
1035
|
+
*,
|
|
1036
|
+
deadline_monotonic: float | None = None,
|
|
923
1037
|
) -> ClaimedMessage | None:
|
|
1038
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
924
1039
|
claim_result_key = self._claim_result_key(processing_queue, claim_id)
|
|
925
|
-
cached_claim =
|
|
1040
|
+
cached_claim = _call_with_drain_deadline(
|
|
1041
|
+
lambda: self._redis_client.get(claim_result_key),
|
|
1042
|
+
deadline_monotonic=deadline_monotonic,
|
|
1043
|
+
)
|
|
1044
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
926
1045
|
if cached_claim is None:
|
|
927
|
-
cached_claim =
|
|
1046
|
+
cached_claim = _call_with_drain_deadline(
|
|
1047
|
+
lambda: self._redis_client.hget(self._claim_result_ids_key(processing_queue), claim_id),
|
|
1048
|
+
deadline_monotonic=deadline_monotonic,
|
|
1049
|
+
)
|
|
1050
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
928
1051
|
if cached_claim is None:
|
|
929
1052
|
return None
|
|
930
1053
|
|
|
@@ -932,7 +1055,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
932
1055
|
try:
|
|
933
1056
|
claim = json.loads(cached_claim_text)
|
|
934
1057
|
except json.JSONDecodeError:
|
|
935
|
-
self._delete_claim_result_key(claim_result_key)
|
|
1058
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
936
1059
|
return None
|
|
937
1060
|
|
|
938
1061
|
if (
|
|
@@ -941,7 +1064,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
941
1064
|
or not isinstance(claim[0], str)
|
|
942
1065
|
or not isinstance(claim[1], str)
|
|
943
1066
|
):
|
|
944
|
-
self._delete_claim_result_key(claim_result_key)
|
|
1067
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
945
1068
|
return None
|
|
946
1069
|
|
|
947
1070
|
stored_message: MessageData = claim[0]
|
|
@@ -949,12 +1072,19 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
949
1072
|
stored_message = stored_message.encode("utf-8")
|
|
950
1073
|
lease_token = claim[1]
|
|
951
1074
|
|
|
952
|
-
self._delete_claim_result_key(claim_result_key)
|
|
953
|
-
self._delete_claim_result_ref(
|
|
1075
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
1076
|
+
self._delete_claim_result_ref(
|
|
1077
|
+
self._claim_result_refs_key(processing_queue),
|
|
1078
|
+
lease_token,
|
|
1079
|
+
deadline_monotonic=deadline_monotonic,
|
|
1080
|
+
)
|
|
1081
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
954
1082
|
return ClaimedMessage(stored_message=stored_message, lease_token=lease_token)
|
|
955
1083
|
|
|
956
|
-
def _is_interrupted(self) -> bool:
|
|
957
|
-
return self._interrupt is not None and self._interrupt.is_interrupted()
|
|
1084
|
+
def _is_interrupted(self, is_interrupted: BaseGracefulInterruptHandler | None = None) -> bool:
|
|
1085
|
+
return (self._interrupt is not None and self._interrupt.is_interrupted()) or (
|
|
1086
|
+
is_interrupted is not None and is_interrupted.is_interrupted()
|
|
1087
|
+
)
|
|
958
1088
|
|
|
959
1089
|
def _drain_pending_claim_ids(
|
|
960
1090
|
self,
|
|
@@ -1010,8 +1140,10 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1010
1140
|
clear = False
|
|
1011
1141
|
try:
|
|
1012
1142
|
try:
|
|
1013
|
-
recover(processing_queue, claim_id)
|
|
1143
|
+
recover(processing_queue, claim_id, deadline_monotonic=deadline_monotonic)
|
|
1014
1144
|
clear = True
|
|
1145
|
+
except _DrainDeadlineExceeded:
|
|
1146
|
+
break
|
|
1015
1147
|
except Exception as exc:
|
|
1016
1148
|
if not is_redis_retryable_exception(exc):
|
|
1017
1149
|
raise
|
|
@@ -54,6 +54,7 @@ from redis_message_queue.interrupt_handler._interface import (
|
|
|
54
54
|
|
|
55
55
|
logger = logging.getLogger(__name__)
|
|
56
56
|
_TClaim = TypeVar("_TClaim", bound=ClaimedMessage | MessageData)
|
|
57
|
+
_TRedisCall = TypeVar("_TRedisCall")
|
|
57
58
|
|
|
58
59
|
_LEASE_DEADLINES_SUFFIX = ":lease_deadlines"
|
|
59
60
|
_LEASE_TOKENS_SUFFIX = ":lease_tokens"
|
|
@@ -71,6 +72,33 @@ _PENDING_OVERLOAD_INITIAL_BACKOFF_SECONDS = 0.010
|
|
|
71
72
|
_PENDING_OVERLOAD_MAX_BACKOFF_SECONDS = 0.500
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
class _DrainDeadlineExceeded(Exception):
|
|
76
|
+
"""Internal sentinel for drain-only pending-claim recovery deadlines."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _raise_if_drain_deadline_expired(deadline_monotonic: float | None) -> None:
|
|
80
|
+
if deadline_monotonic is not None and asyncio.get_running_loop().time() >= deadline_monotonic:
|
|
81
|
+
raise _DrainDeadlineExceeded
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _call_with_drain_deadline(
|
|
85
|
+
call: Callable[[], Awaitable[_TRedisCall]],
|
|
86
|
+
*,
|
|
87
|
+
deadline_monotonic: float | None,
|
|
88
|
+
) -> _TRedisCall:
|
|
89
|
+
if deadline_monotonic is None:
|
|
90
|
+
return await call()
|
|
91
|
+
|
|
92
|
+
remaining_seconds = deadline_monotonic - asyncio.get_running_loop().time()
|
|
93
|
+
if remaining_seconds <= 0:
|
|
94
|
+
raise _DrainDeadlineExceeded
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return await asyncio.wait_for(call(), timeout=remaining_seconds)
|
|
98
|
+
except TimeoutError as exc:
|
|
99
|
+
raise _DrainDeadlineExceeded from exc
|
|
100
|
+
|
|
101
|
+
|
|
74
102
|
def _coerce_lua_count(value: object) -> int:
|
|
75
103
|
if isinstance(value, bytes):
|
|
76
104
|
value = value.decode("utf-8")
|
|
@@ -521,6 +549,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
521
549
|
claim_message: Callable[[str, str, str], Awaitable[_TClaim | None]],
|
|
522
550
|
non_blocking_retry_log: str,
|
|
523
551
|
polling_retry_log: str,
|
|
552
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
524
553
|
) -> _TClaim | None:
|
|
525
554
|
while True:
|
|
526
555
|
pending_claim_id = self._acquire_pending_claim_id(to_queue)
|
|
@@ -548,7 +577,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
548
577
|
await self._emit_event("claim_reclaim", "success", claim_id=pending_claim_id)
|
|
549
578
|
return recovered_claim
|
|
550
579
|
|
|
551
|
-
if self._is_interrupted():
|
|
580
|
+
if self._is_interrupted(is_interrupted):
|
|
552
581
|
return None
|
|
553
582
|
|
|
554
583
|
pending_claim_id_to_share: str | None = None
|
|
@@ -571,7 +600,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
571
600
|
error=exc,
|
|
572
601
|
)
|
|
573
602
|
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
574
|
-
if self._is_interrupted():
|
|
603
|
+
if self._is_interrupted(is_interrupted):
|
|
575
604
|
pending_claim_id_to_share = claim_id
|
|
576
605
|
return None
|
|
577
606
|
try:
|
|
@@ -604,7 +633,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
604
633
|
claim_may_need_recovery = False
|
|
605
634
|
last_retryable_exception: Exception | None = None
|
|
606
635
|
while True:
|
|
607
|
-
if self._is_interrupted():
|
|
636
|
+
if self._is_interrupted(is_interrupted):
|
|
608
637
|
if claim_may_need_recovery:
|
|
609
638
|
pending_claim_id_to_share = claim_id
|
|
610
639
|
return None
|
|
@@ -637,7 +666,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
637
666
|
remaining = deadline - loop.time()
|
|
638
667
|
if remaining <= 0:
|
|
639
668
|
if last_retryable_exception is not None:
|
|
640
|
-
if self._is_interrupted():
|
|
669
|
+
if self._is_interrupted(is_interrupted):
|
|
641
670
|
if claim_may_need_recovery:
|
|
642
671
|
pending_claim_id_to_share = claim_id
|
|
643
672
|
return None
|
|
@@ -672,11 +701,36 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
672
701
|
async def wait_for_message_and_move(self, from_queue: str, to_queue: str) -> ClaimedMessage | MessageData | None:
|
|
673
702
|
if self._is_interrupted():
|
|
674
703
|
return None
|
|
704
|
+
return await self._wait_for_message_and_move_interruptible(from_queue, to_queue)
|
|
705
|
+
|
|
706
|
+
async def _wait_for_message_and_move_interruptible(
|
|
707
|
+
self,
|
|
708
|
+
from_queue: str,
|
|
709
|
+
to_queue: str,
|
|
710
|
+
*,
|
|
711
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
712
|
+
) -> ClaimedMessage | MessageData | None:
|
|
713
|
+
if self._is_interrupted(is_interrupted):
|
|
714
|
+
return None
|
|
675
715
|
if self._message_visibility_timeout_seconds is not None:
|
|
676
|
-
return await self._wait_for_message_with_visibility_timeout(
|
|
677
|
-
|
|
716
|
+
return await self._wait_for_message_with_visibility_timeout(
|
|
717
|
+
from_queue,
|
|
718
|
+
to_queue,
|
|
719
|
+
is_interrupted=is_interrupted,
|
|
720
|
+
)
|
|
721
|
+
return await self._wait_for_message_without_visibility_timeout(
|
|
722
|
+
from_queue,
|
|
723
|
+
to_queue,
|
|
724
|
+
is_interrupted=is_interrupted,
|
|
725
|
+
)
|
|
678
726
|
|
|
679
|
-
async def _wait_for_message_without_visibility_timeout(
|
|
727
|
+
async def _wait_for_message_without_visibility_timeout(
|
|
728
|
+
self,
|
|
729
|
+
from_queue: str,
|
|
730
|
+
to_queue: str,
|
|
731
|
+
*,
|
|
732
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
733
|
+
) -> MessageData | None:
|
|
680
734
|
return await self._wait_for_claim(
|
|
681
735
|
from_queue,
|
|
682
736
|
to_queue,
|
|
@@ -690,9 +744,16 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
690
744
|
"Transient error during non-visibility-timeout non-blocking claim, retrying once to recover claim: %s"
|
|
691
745
|
),
|
|
692
746
|
polling_retry_log="Transient error during non-visibility-timeout claim poll, will retry: %s",
|
|
747
|
+
is_interrupted=is_interrupted,
|
|
693
748
|
)
|
|
694
749
|
|
|
695
|
-
async def _wait_for_message_with_visibility_timeout(
|
|
750
|
+
async def _wait_for_message_with_visibility_timeout(
|
|
751
|
+
self,
|
|
752
|
+
from_queue: str,
|
|
753
|
+
to_queue: str,
|
|
754
|
+
*,
|
|
755
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
756
|
+
) -> ClaimedMessage | None:
|
|
696
757
|
return await self._wait_for_claim(
|
|
697
758
|
from_queue,
|
|
698
759
|
to_queue,
|
|
@@ -706,6 +767,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
706
767
|
"Transient error during visibility-timeout non-blocking claim, retrying once to recover claim: %s"
|
|
707
768
|
),
|
|
708
769
|
polling_retry_log="Transient error during visibility-timeout claim poll, will retry: %s",
|
|
770
|
+
is_interrupted=is_interrupted,
|
|
709
771
|
)
|
|
710
772
|
|
|
711
773
|
async def _claim_message_without_visibility_timeout(
|
|
@@ -830,16 +892,37 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
830
892
|
def _claim_result_ttl_ms(self) -> str:
|
|
831
893
|
return str(max(self._message_wait_interval_seconds, 120) * 1000)
|
|
832
894
|
|
|
833
|
-
async def _delete_claim_result_key(
|
|
895
|
+
async def _delete_claim_result_key(
|
|
896
|
+
self,
|
|
897
|
+
claim_result_key: str,
|
|
898
|
+
*,
|
|
899
|
+
deadline_monotonic: float | None = None,
|
|
900
|
+
) -> None:
|
|
834
901
|
try:
|
|
835
|
-
await
|
|
902
|
+
await _call_with_drain_deadline(
|
|
903
|
+
lambda: self._redis_client.delete(claim_result_key),
|
|
904
|
+
deadline_monotonic=deadline_monotonic,
|
|
905
|
+
)
|
|
906
|
+
except _DrainDeadlineExceeded:
|
|
907
|
+
raise
|
|
836
908
|
except Exception:
|
|
837
909
|
# Claim-result keys have bounded TTLs; this cleanup is intentionally best-effort.
|
|
838
910
|
logger.warning("Failed to delete claim result key %s", claim_result_key, exc_info=True)
|
|
839
911
|
|
|
840
|
-
async def _delete_claim_result_ref(
|
|
912
|
+
async def _delete_claim_result_ref(
|
|
913
|
+
self,
|
|
914
|
+
claim_result_refs_key: str,
|
|
915
|
+
lease_token: str,
|
|
916
|
+
*,
|
|
917
|
+
deadline_monotonic: float | None = None,
|
|
918
|
+
) -> None:
|
|
841
919
|
try:
|
|
842
|
-
await
|
|
920
|
+
await _call_with_drain_deadline(
|
|
921
|
+
lambda: self._redis_client.hdel(claim_result_refs_key, lease_token),
|
|
922
|
+
deadline_monotonic=deadline_monotonic,
|
|
923
|
+
)
|
|
924
|
+
except _DrainDeadlineExceeded:
|
|
925
|
+
raise
|
|
843
926
|
except Exception:
|
|
844
927
|
# Claim-result refs have bounded TTLs; this cleanup is intentionally best-effort.
|
|
845
928
|
logger.warning(
|
|
@@ -903,25 +986,48 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
903
986
|
self,
|
|
904
987
|
processing_queue: str,
|
|
905
988
|
claim_id: str,
|
|
989
|
+
*,
|
|
990
|
+
deadline_monotonic: float | None = None,
|
|
906
991
|
) -> MessageData | None:
|
|
992
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
907
993
|
claim_result_key = self._claim_result_key(processing_queue, claim_id)
|
|
908
|
-
cached_claim = await
|
|
994
|
+
cached_claim = await _call_with_drain_deadline(
|
|
995
|
+
lambda: self._redis_client.get(claim_result_key),
|
|
996
|
+
deadline_monotonic=deadline_monotonic,
|
|
997
|
+
)
|
|
998
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
909
999
|
if cached_claim is None:
|
|
910
|
-
cached_claim = await
|
|
1000
|
+
cached_claim = await _call_with_drain_deadline(
|
|
1001
|
+
lambda: self._redis_client.hget(self._claim_result_ids_key(processing_queue), claim_id),
|
|
1002
|
+
deadline_monotonic=deadline_monotonic,
|
|
1003
|
+
)
|
|
1004
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
911
1005
|
if cached_claim is None:
|
|
912
1006
|
return None
|
|
913
|
-
await self._delete_claim_result_key(claim_result_key)
|
|
1007
|
+
await self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
1008
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
914
1009
|
return cached_claim
|
|
915
1010
|
|
|
916
1011
|
async def _recover_pending_visibility_timeout_claim(
|
|
917
1012
|
self,
|
|
918
1013
|
processing_queue: str,
|
|
919
1014
|
claim_id: str,
|
|
1015
|
+
*,
|
|
1016
|
+
deadline_monotonic: float | None = None,
|
|
920
1017
|
) -> ClaimedMessage | None:
|
|
1018
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
921
1019
|
claim_result_key = self._claim_result_key(processing_queue, claim_id)
|
|
922
|
-
cached_claim = await
|
|
1020
|
+
cached_claim = await _call_with_drain_deadline(
|
|
1021
|
+
lambda: self._redis_client.get(claim_result_key),
|
|
1022
|
+
deadline_monotonic=deadline_monotonic,
|
|
1023
|
+
)
|
|
1024
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
923
1025
|
if cached_claim is None:
|
|
924
|
-
cached_claim = await
|
|
1026
|
+
cached_claim = await _call_with_drain_deadline(
|
|
1027
|
+
lambda: self._redis_client.hget(self._claim_result_ids_key(processing_queue), claim_id),
|
|
1028
|
+
deadline_monotonic=deadline_monotonic,
|
|
1029
|
+
)
|
|
1030
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
925
1031
|
if cached_claim is None:
|
|
926
1032
|
return None
|
|
927
1033
|
|
|
@@ -929,7 +1035,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
929
1035
|
try:
|
|
930
1036
|
claim = json.loads(cached_claim_text)
|
|
931
1037
|
except json.JSONDecodeError:
|
|
932
|
-
await self._delete_claim_result_key(claim_result_key)
|
|
1038
|
+
await self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
933
1039
|
return None
|
|
934
1040
|
|
|
935
1041
|
if (
|
|
@@ -938,7 +1044,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
938
1044
|
or not isinstance(claim[0], str)
|
|
939
1045
|
or not isinstance(claim[1], str)
|
|
940
1046
|
):
|
|
941
|
-
await self._delete_claim_result_key(claim_result_key)
|
|
1047
|
+
await self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
942
1048
|
return None
|
|
943
1049
|
|
|
944
1050
|
stored_message: MessageData = claim[0]
|
|
@@ -946,12 +1052,20 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
946
1052
|
stored_message = stored_message.encode("utf-8")
|
|
947
1053
|
lease_token = claim[1]
|
|
948
1054
|
|
|
949
|
-
await self._delete_claim_result_key(claim_result_key)
|
|
950
|
-
|
|
1055
|
+
await self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
1056
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1057
|
+
await self._delete_claim_result_ref(
|
|
1058
|
+
self._claim_result_refs_key(processing_queue),
|
|
1059
|
+
lease_token,
|
|
1060
|
+
deadline_monotonic=deadline_monotonic,
|
|
1061
|
+
)
|
|
1062
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
951
1063
|
return ClaimedMessage(stored_message=stored_message, lease_token=lease_token)
|
|
952
1064
|
|
|
953
|
-
def _is_interrupted(self) -> bool:
|
|
954
|
-
return self._interrupt is not None and self._interrupt.is_interrupted()
|
|
1065
|
+
def _is_interrupted(self, is_interrupted: BaseGracefulInterruptHandler | None = None) -> bool:
|
|
1066
|
+
return (self._interrupt is not None and self._interrupt.is_interrupted()) or (
|
|
1067
|
+
is_interrupted is not None and is_interrupted.is_interrupted()
|
|
1068
|
+
)
|
|
955
1069
|
|
|
956
1070
|
async def _drain_pending_claim_ids(
|
|
957
1071
|
self,
|
|
@@ -1006,8 +1120,10 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1006
1120
|
clear = False
|
|
1007
1121
|
try:
|
|
1008
1122
|
try:
|
|
1009
|
-
await recover(processing_queue, claim_id)
|
|
1123
|
+
await recover(processing_queue, claim_id, deadline_monotonic=deadline_monotonic)
|
|
1010
1124
|
clear = True
|
|
1125
|
+
except _DrainDeadlineExceeded:
|
|
1126
|
+
break
|
|
1011
1127
|
except Exception as exc:
|
|
1012
1128
|
if not is_redis_retryable_exception(exc):
|
|
1013
1129
|
raise
|
|
@@ -74,6 +74,30 @@ def _warning_exception_name(exc: BaseException) -> str:
|
|
|
74
74
|
return type(exc).__name__
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def _find_non_string_dict_keys(value: object) -> list[object]:
|
|
78
|
+
non_str_keys: list[object] = []
|
|
79
|
+
seen: set[int] = set()
|
|
80
|
+
|
|
81
|
+
def visit(current: object) -> None:
|
|
82
|
+
if not isinstance(current, (dict, list, tuple)):
|
|
83
|
+
return
|
|
84
|
+
current_id = id(current)
|
|
85
|
+
if current_id in seen:
|
|
86
|
+
return
|
|
87
|
+
seen.add(current_id)
|
|
88
|
+
if isinstance(current, dict):
|
|
89
|
+
for key, child in current.items():
|
|
90
|
+
if not isinstance(key, str):
|
|
91
|
+
non_str_keys.append(key)
|
|
92
|
+
visit(child)
|
|
93
|
+
return
|
|
94
|
+
for child in current:
|
|
95
|
+
visit(child)
|
|
96
|
+
|
|
97
|
+
visit(value)
|
|
98
|
+
return non_str_keys
|
|
99
|
+
|
|
100
|
+
|
|
77
101
|
class _TaskBaseException(Exception):
|
|
78
102
|
def __init__(self, original: BaseException):
|
|
79
103
|
super().__init__(str(original))
|
|
@@ -285,6 +309,14 @@ class _StopEventInterrupt(BaseGracefulInterruptHandler):
|
|
|
285
309
|
return self._stop_event.is_set()
|
|
286
310
|
|
|
287
311
|
|
|
312
|
+
class _DrainInterrupt(BaseGracefulInterruptHandler):
|
|
313
|
+
def __init__(self, is_draining: Callable[[], bool]) -> None:
|
|
314
|
+
self._is_draining = is_draining
|
|
315
|
+
|
|
316
|
+
def is_interrupted(self) -> bool:
|
|
317
|
+
return self._is_draining()
|
|
318
|
+
|
|
319
|
+
|
|
288
320
|
class _LeaseHeartbeat:
|
|
289
321
|
def __init__(
|
|
290
322
|
self,
|
|
@@ -758,11 +790,10 @@ class RedisMessageQueue:
|
|
|
758
790
|
"""Publish a message.
|
|
759
791
|
|
|
760
792
|
Dict messages are serialized via ``json.dumps(message, sort_keys=True)``.
|
|
761
|
-
All
|
|
793
|
+
All dict keys must be strings; non-string keys raise
|
|
762
794
|
``TypeError`` to avoid silent ``json.dumps`` coercion that would
|
|
763
795
|
collapse distinct keys into the same dedup key (e.g. ``{1: "x"}``
|
|
764
|
-
vs ``{"1": "x"}``).
|
|
765
|
-
dicts follow ``json.dumps`` defaults.
|
|
796
|
+
vs ``{"1": "x"}``).
|
|
766
797
|
"""
|
|
767
798
|
async with self._publish_lock:
|
|
768
799
|
if self._drained:
|
|
@@ -773,7 +804,7 @@ class RedisMessageQueue:
|
|
|
773
804
|
if not isinstance(message, (str, dict)):
|
|
774
805
|
raise TypeError(f"'message' must be a str or dict, got {type(message).__name__}")
|
|
775
806
|
if isinstance(message, dict):
|
|
776
|
-
non_str_keys =
|
|
807
|
+
non_str_keys = _find_non_string_dict_keys(message)
|
|
777
808
|
if non_str_keys:
|
|
778
809
|
raise TypeError(
|
|
779
810
|
"'message' dict keys must all be strings; "
|
|
@@ -846,10 +877,7 @@ class RedisMessageQueue:
|
|
|
846
877
|
yield None
|
|
847
878
|
return
|
|
848
879
|
try:
|
|
849
|
-
claimed_message = await self.
|
|
850
|
-
self.key.pending,
|
|
851
|
-
self.key.processing,
|
|
852
|
-
)
|
|
880
|
+
claimed_message = await self._wait_for_message_and_move()
|
|
853
881
|
except Exception as exc:
|
|
854
882
|
await self._emit_event(
|
|
855
883
|
"claim",
|
|
@@ -1027,6 +1055,19 @@ class RedisMessageQueue:
|
|
|
1027
1055
|
else:
|
|
1028
1056
|
await _await_suppressing_external_cancellation(lease_heartbeat.stop())
|
|
1029
1057
|
|
|
1058
|
+
async def _wait_for_message_and_move(self) -> ClaimedMessage | MessageData | None:
|
|
1059
|
+
interruptible_wait = getattr(self._redis, "_wait_for_message_and_move_interruptible", None)
|
|
1060
|
+
if callable(interruptible_wait):
|
|
1061
|
+
return await interruptible_wait(
|
|
1062
|
+
self.key.pending,
|
|
1063
|
+
self.key.processing,
|
|
1064
|
+
is_interrupted=_DrainInterrupt(lambda: self._draining),
|
|
1065
|
+
)
|
|
1066
|
+
return await self._redis.wait_for_message_and_move(
|
|
1067
|
+
self.key.pending,
|
|
1068
|
+
self.key.processing,
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1030
1071
|
async def _move_processed_message(
|
|
1031
1072
|
self,
|
|
1032
1073
|
destination_queue: str,
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -73,6 +73,30 @@ def _warning_exception_name(exc: BaseException) -> str:
|
|
|
73
73
|
return type(exc).__name__
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _find_non_string_dict_keys(value: object) -> list[object]:
|
|
77
|
+
non_str_keys: list[object] = []
|
|
78
|
+
seen: set[int] = set()
|
|
79
|
+
|
|
80
|
+
def visit(current: object) -> None:
|
|
81
|
+
if not isinstance(current, (dict, list, tuple)):
|
|
82
|
+
return
|
|
83
|
+
current_id = id(current)
|
|
84
|
+
if current_id in seen:
|
|
85
|
+
return
|
|
86
|
+
seen.add(current_id)
|
|
87
|
+
if isinstance(current, dict):
|
|
88
|
+
for key, child in current.items():
|
|
89
|
+
if not isinstance(key, str):
|
|
90
|
+
non_str_keys.append(key)
|
|
91
|
+
visit(child)
|
|
92
|
+
return
|
|
93
|
+
for child in current:
|
|
94
|
+
visit(child)
|
|
95
|
+
|
|
96
|
+
visit(value)
|
|
97
|
+
return non_str_keys
|
|
98
|
+
|
|
99
|
+
|
|
76
100
|
def _validate_heartbeat_interval_seconds(
|
|
77
101
|
heartbeat_interval_seconds: int | float | None,
|
|
78
102
|
visibility_timeout_seconds: int | None,
|
|
@@ -235,6 +259,14 @@ class _StopEventInterrupt(BaseGracefulInterruptHandler):
|
|
|
235
259
|
return self._stop_event.is_set()
|
|
236
260
|
|
|
237
261
|
|
|
262
|
+
class _DrainInterrupt(BaseGracefulInterruptHandler):
|
|
263
|
+
def __init__(self, is_draining: Callable[[], bool]) -> None:
|
|
264
|
+
self._is_draining = is_draining
|
|
265
|
+
|
|
266
|
+
def is_interrupted(self) -> bool:
|
|
267
|
+
return self._is_draining()
|
|
268
|
+
|
|
269
|
+
|
|
238
270
|
class _LeaseHeartbeat:
|
|
239
271
|
def __init__(
|
|
240
272
|
self,
|
|
@@ -711,12 +743,10 @@ class RedisMessageQueue:
|
|
|
711
743
|
"""Publish a message.
|
|
712
744
|
|
|
713
745
|
Dict messages are serialized via ``json.dumps(message, sort_keys=True)``.
|
|
714
|
-
All
|
|
746
|
+
All dict keys must be strings; non-string keys raise
|
|
715
747
|
``TypeError`` to avoid silent ``json.dumps`` coercion that would
|
|
716
748
|
collapse distinct keys into the same dedup key (e.g. ``{1: "x"}``
|
|
717
|
-
vs ``{"1": "x"}``).
|
|
718
|
-
dicts follow ``json.dumps`` defaults (e.g. nested non-string keys
|
|
719
|
-
are silently coerced: integer keys become strings).
|
|
749
|
+
vs ``{"1": "x"}``).
|
|
720
750
|
"""
|
|
721
751
|
with self._publish_lock:
|
|
722
752
|
if self._drained.is_set():
|
|
@@ -727,7 +757,7 @@ class RedisMessageQueue:
|
|
|
727
757
|
if not isinstance(message, (str, dict)):
|
|
728
758
|
raise TypeError(f"'message' must be a str or dict, got {type(message).__name__}")
|
|
729
759
|
if isinstance(message, dict):
|
|
730
|
-
non_str_keys =
|
|
760
|
+
non_str_keys = _find_non_string_dict_keys(message)
|
|
731
761
|
if non_str_keys:
|
|
732
762
|
raise TypeError(
|
|
733
763
|
"'message' dict keys must all be strings; "
|
|
@@ -808,10 +838,7 @@ class RedisMessageQueue:
|
|
|
808
838
|
yield None
|
|
809
839
|
return
|
|
810
840
|
try:
|
|
811
|
-
claimed_message = self.
|
|
812
|
-
self.key.pending,
|
|
813
|
-
self.key.processing,
|
|
814
|
-
)
|
|
841
|
+
claimed_message = self._wait_for_message_and_move()
|
|
815
842
|
except Exception as exc:
|
|
816
843
|
self._emit_event(
|
|
817
844
|
"claim",
|
|
@@ -979,6 +1006,19 @@ class RedisMessageQueue:
|
|
|
979
1006
|
if lease_heartbeat is not None:
|
|
980
1007
|
lease_heartbeat.stop()
|
|
981
1008
|
|
|
1009
|
+
def _wait_for_message_and_move(self) -> ClaimedMessage | MessageData | None:
|
|
1010
|
+
interruptible_wait = getattr(self._redis, "_wait_for_message_and_move_interruptible", None)
|
|
1011
|
+
if callable(interruptible_wait):
|
|
1012
|
+
return interruptible_wait(
|
|
1013
|
+
self.key.pending,
|
|
1014
|
+
self.key.processing,
|
|
1015
|
+
is_interrupted=_DrainInterrupt(lambda: self._draining),
|
|
1016
|
+
)
|
|
1017
|
+
return self._redis.wait_for_message_and_move(
|
|
1018
|
+
self.key.pending,
|
|
1019
|
+
self.key.processing,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
982
1022
|
def _move_processed_message(
|
|
983
1023
|
self,
|
|
984
1024
|
destination_queue: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.1}/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
|