redis-message-queue 8.0.0__tar.gz → 8.0.2__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.2}/PKG-INFO +2 -2
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/README.md +1 -1
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/pyproject.toml +1 -1
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/__init__.py +2 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_config.py +79 -25
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_event.py +4 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_exceptions.py +4 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_redis_gateway.py +222 -32
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_stored_message.py +49 -30
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/asyncio/__init__.py +2 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/asyncio/_redis_gateway.py +205 -31
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/asyncio/redis_message_queue.py +142 -84
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/redis_message_queue.py +152 -93
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/LICENSE +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/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.2
|
|
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)
|
|
@@ -5,6 +5,7 @@ from redis_message_queue._exceptions import (
|
|
|
5
5
|
ConfigurationError,
|
|
6
6
|
GatewayContractError,
|
|
7
7
|
LuaScriptError,
|
|
8
|
+
MalformedStoredMessageError,
|
|
8
9
|
QueueBackpressureError,
|
|
9
10
|
QueueDrainedError,
|
|
10
11
|
RedisMessageQueueError,
|
|
@@ -33,6 +34,7 @@ __all__ = [
|
|
|
33
34
|
"ConfigurationError",
|
|
34
35
|
"GatewayContractError",
|
|
35
36
|
"LuaScriptError",
|
|
37
|
+
"MalformedStoredMessageError",
|
|
36
38
|
"QueueBackpressureError",
|
|
37
39
|
"QueueDrainedError",
|
|
38
40
|
"CleanupFailedError",
|
|
@@ -739,6 +739,26 @@ local function redis_message_queue_decode_claim(cached_claim)
|
|
|
739
739
|
return nil
|
|
740
740
|
end
|
|
741
741
|
|
|
742
|
+
local function redis_message_queue_decode_envelope(stored)
|
|
743
|
+
local prefix = string.char(30) .. 'RMQ1:'
|
|
744
|
+
if type(stored) ~= 'string' or string.sub(stored, 1, string.len(prefix)) ~= prefix then
|
|
745
|
+
return nil
|
|
746
|
+
end
|
|
747
|
+
local ok, envelope = pcall(cjson.decode, string.sub(stored, string.len(prefix) + 1))
|
|
748
|
+
if ok and type(envelope) == 'table' and type(envelope['id']) == 'string' then
|
|
749
|
+
return envelope
|
|
750
|
+
end
|
|
751
|
+
return nil
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
local function redis_message_queue_message_id(stored)
|
|
755
|
+
local envelope = redis_message_queue_decode_envelope(stored)
|
|
756
|
+
if envelope then
|
|
757
|
+
return envelope['id']
|
|
758
|
+
end
|
|
759
|
+
return ''
|
|
760
|
+
end
|
|
761
|
+
|
|
742
762
|
local time = redis.call('TIME')
|
|
743
763
|
local now_ms = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
744
764
|
|
|
@@ -779,6 +799,7 @@ end
|
|
|
779
799
|
-- With a single consumer polling at default interval, 1000 expired leases drain in ~2.5s.
|
|
780
800
|
local expired = redis.call('ZRANGEBYSCORE', KEYS[3], '-inf', now_ms, 'LIMIT', 0, 100)
|
|
781
801
|
local to_requeue = {}
|
|
802
|
+
local reclaimed_events = {}
|
|
782
803
|
for i = #expired, 1, -1 do
|
|
783
804
|
local expired_lease_token = redis.call('HGET', KEYS[4], expired[i])
|
|
784
805
|
redis.call('ZREM', KEYS[3], expired[i])
|
|
@@ -800,13 +821,14 @@ for i = #expired, 1, -1 do
|
|
|
800
821
|
end
|
|
801
822
|
if redis.call('LREM', KEYS[2], 1, expired[i]) == 1 then
|
|
802
823
|
table.insert(to_requeue, expired[i])
|
|
824
|
+
local delivery_count = redis.call('HGET', KEYS[6], expired[i])
|
|
825
|
+
table.insert(reclaimed_events, {redis_message_queue_message_id(expired[i]), tostring(delivery_count or '0')})
|
|
803
826
|
end
|
|
804
827
|
end
|
|
805
828
|
if #to_requeue > 0 then
|
|
806
829
|
redis.call('RPUSH', KEYS[1], unpack(to_requeue))
|
|
807
830
|
end
|
|
808
|
-
local
|
|
809
|
-
local dead_lettered_count = 0
|
|
831
|
+
local dead_lettered_events = {}
|
|
810
832
|
|
|
811
833
|
local function store_claim_and_return(stored)
|
|
812
834
|
-- pcall guards against OOM mid-write: compensate by returning message to pending
|
|
@@ -819,7 +841,7 @@ local function store_claim_and_return(stored)
|
|
|
819
841
|
redis.call('HSET', KEYS[9], lease_token, KEYS[8])
|
|
820
842
|
redis.call('HSET', KEYS[10], ARGV[4], claim_payload)
|
|
821
843
|
redis.call('HSET', KEYS[11], lease_token, ARGV[4])
|
|
822
|
-
return {stored, lease_token,
|
|
844
|
+
return {stored, lease_token, reclaimed_events, dead_lettered_events}
|
|
823
845
|
end)
|
|
824
846
|
if not ok then
|
|
825
847
|
redis.call('LREM', KEYS[2], 1, stored)
|
|
@@ -835,36 +857,28 @@ while claim_attempts < 100 do
|
|
|
835
857
|
|
|
836
858
|
local stored = redis.call('LMOVE', KEYS[1], KEYS[2], 'RIGHT', 'LEFT')
|
|
837
859
|
if not stored then
|
|
838
|
-
return {'', '',
|
|
860
|
+
return {'', '', reclaimed_events, dead_lettered_events}
|
|
839
861
|
end
|
|
840
862
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
local ok, envelope = pcall(cjson.decode, string.sub(stored, string.len(prefix) + 1))
|
|
852
|
-
if ok and type(envelope) == 'table' and type(envelope['id']) == 'string'
|
|
853
|
-
and type(envelope['payload']) == 'string' then
|
|
854
|
-
dead_letter_value = envelope['payload']
|
|
855
|
-
end
|
|
856
|
-
end
|
|
857
|
-
redis.call('LPUSH', KEYS[7], dead_letter_value)
|
|
858
|
-
dead_lettered_count = dead_lettered_count + 1
|
|
859
|
-
else
|
|
860
|
-
return store_claim_and_return(stored)
|
|
863
|
+
local count = redis.call('HINCRBY', KEYS[6], stored, 1)
|
|
864
|
+
if max_delivery_count > 0 and count > max_delivery_count then
|
|
865
|
+
redis.call('LREM', KEYS[2], 1, stored)
|
|
866
|
+
redis.call('HDEL', KEYS[6], stored)
|
|
867
|
+
-- Strip envelope to store raw payload in DLQ, consistent with completed/failed queues.
|
|
868
|
+
-- The per-delivery UUID in the envelope is lost; see README dead-letter notes.
|
|
869
|
+
local dead_letter_value = stored
|
|
870
|
+
local envelope = redis_message_queue_decode_envelope(stored)
|
|
871
|
+
if envelope and type(envelope['payload']) == 'string' then
|
|
872
|
+
dead_letter_value = envelope['payload']
|
|
861
873
|
end
|
|
874
|
+
redis.call('LPUSH', KEYS[7], dead_letter_value)
|
|
875
|
+
table.insert(dead_lettered_events, {redis_message_queue_message_id(stored), tostring(count)})
|
|
862
876
|
else
|
|
863
877
|
return store_claim_and_return(stored)
|
|
864
878
|
end
|
|
865
879
|
end
|
|
866
880
|
|
|
867
|
-
return {'', '',
|
|
881
|
+
return {'', '', reclaimed_events, dead_lettered_events}
|
|
868
882
|
"""
|
|
869
883
|
)
|
|
870
884
|
|
|
@@ -1037,6 +1051,46 @@ return removed
|
|
|
1037
1051
|
"""
|
|
1038
1052
|
)
|
|
1039
1053
|
|
|
1054
|
+
CLEANUP_DRAINED_LEASE_TOKEN_COUNTER_LUA_SCRIPT = (
|
|
1055
|
+
_LUA_KEY_TYPE_GUARD
|
|
1056
|
+
+ """
|
|
1057
|
+
local err = redis_message_queue_require_type(KEYS[1], 'list')
|
|
1058
|
+
if err then
|
|
1059
|
+
return err
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
local err = redis_message_queue_require_type(KEYS[2], 'zset')
|
|
1063
|
+
if err then
|
|
1064
|
+
return err
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
local err = redis_message_queue_require_type(KEYS[3], 'hash')
|
|
1068
|
+
if err then
|
|
1069
|
+
return err
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
local err = redis_message_queue_require_type(KEYS[4], 'hash')
|
|
1073
|
+
if err then
|
|
1074
|
+
return err
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
local err = redis_message_queue_require_type(KEYS[5], 'string')
|
|
1078
|
+
if err then
|
|
1079
|
+
return err
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
if redis.call('LLEN', KEYS[1]) == 0
|
|
1083
|
+
and redis.call('ZCARD', KEYS[2]) == 0
|
|
1084
|
+
and redis.call('HLEN', KEYS[3]) == 0
|
|
1085
|
+
and redis.call('HLEN', KEYS[4]) == 0 then
|
|
1086
|
+
redis.call('DEL', KEYS[5])
|
|
1087
|
+
return 1
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
return 0
|
|
1091
|
+
"""
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1040
1094
|
RENEW_MESSAGE_LEASE_LUA_SCRIPT = (
|
|
1041
1095
|
_LUA_KEY_TYPE_GUARD
|
|
1042
1096
|
+ """
|
|
@@ -52,6 +52,10 @@ class QueueEvent:
|
|
|
52
52
|
"""a diagnostic hash of the lease token when visibility timeout is enabled"""
|
|
53
53
|
destination_queue: str | None = None
|
|
54
54
|
"""the queue a message was moved to, when applicable"""
|
|
55
|
+
delivery_count: int | None = None
|
|
56
|
+
"""the number of delivery attempts recorded for this message, when applicable"""
|
|
57
|
+
max_delivery_count: int | None = None
|
|
58
|
+
"""the configured delivery-attempt threshold, when applicable"""
|
|
55
59
|
exception_type: str | None = None
|
|
56
60
|
"""
|
|
57
61
|
type name of the raised exception for metrics labels (e.g., 'TimeoutError');
|
|
@@ -21,6 +21,10 @@ class CleanupFailedError(RedisMessageQueueError):
|
|
|
21
21
|
"""Cleanup after handler completion failed."""
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class MalformedStoredMessageError(RedisMessageQueueError):
|
|
25
|
+
"""Stored value starts with the RMQ envelope prefix but is not a valid envelope."""
|
|
26
|
+
|
|
27
|
+
|
|
24
28
|
class QueueBackpressureError(RedisMessageQueueError):
|
|
25
29
|
"""Publish rejected because the pending queue is at its configured limit."""
|
|
26
30
|
|
{redis_message_queue-8.0.0 → redis_message_queue-8.0.2}/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
|
|
@@ -15,6 +16,7 @@ from redis_message_queue._config import (
|
|
|
15
16
|
ADD_MESSAGE_LUA_SCRIPT,
|
|
16
17
|
CLAIM_MESSAGE_LUA_SCRIPT,
|
|
17
18
|
CLAIM_MESSAGE_WITH_VISIBILITY_TIMEOUT_LUA_SCRIPT,
|
|
19
|
+
CLEANUP_DRAINED_LEASE_TOKEN_COUNTER_LUA_SCRIPT,
|
|
18
20
|
DEFAULT_MESSAGE_DEDUPLICATION_LOG_TTL,
|
|
19
21
|
DEFAULT_MESSAGE_WAIT_INTERVAL_SECONDS,
|
|
20
22
|
DEFAULT_PENDING_OVERLOAD_BLOCK_TIMEOUT_SECONDS,
|
|
@@ -54,6 +56,8 @@ from redis_message_queue.interrupt_handler._interface import (
|
|
|
54
56
|
|
|
55
57
|
logger = logging.getLogger(__name__)
|
|
56
58
|
_TClaim = TypeVar("_TClaim", bound=ClaimedMessage | MessageData)
|
|
59
|
+
_TRedisCall = TypeVar("_TRedisCall")
|
|
60
|
+
_MessageAttemptEvent = tuple[str | None, int]
|
|
57
61
|
|
|
58
62
|
_LEASE_DEADLINES_SUFFIX = ":lease_deadlines"
|
|
59
63
|
_LEASE_TOKENS_SUFFIX = ":lease_tokens"
|
|
@@ -71,6 +75,49 @@ _PENDING_OVERLOAD_INITIAL_BACKOFF_SECONDS = 0.010
|
|
|
71
75
|
_PENDING_OVERLOAD_MAX_BACKOFF_SECONDS = 0.500
|
|
72
76
|
|
|
73
77
|
|
|
78
|
+
class _DrainDeadlineExceeded(Exception):
|
|
79
|
+
"""Internal sentinel for drain-only pending-claim recovery deadlines."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _raise_if_drain_deadline_expired(deadline_monotonic: float | None) -> None:
|
|
83
|
+
if deadline_monotonic is not None and time.monotonic() >= deadline_monotonic:
|
|
84
|
+
raise _DrainDeadlineExceeded
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _call_with_drain_deadline(
|
|
88
|
+
call: Callable[[], _TRedisCall],
|
|
89
|
+
*,
|
|
90
|
+
deadline_monotonic: float | None,
|
|
91
|
+
) -> _TRedisCall:
|
|
92
|
+
if deadline_monotonic is None:
|
|
93
|
+
return call()
|
|
94
|
+
|
|
95
|
+
remaining_seconds = deadline_monotonic - time.monotonic()
|
|
96
|
+
if remaining_seconds <= 0:
|
|
97
|
+
raise _DrainDeadlineExceeded
|
|
98
|
+
|
|
99
|
+
result_queue: queue.Queue[tuple[bool, object]] = queue.Queue(maxsize=1)
|
|
100
|
+
|
|
101
|
+
def run_call() -> None:
|
|
102
|
+
try:
|
|
103
|
+
result = call()
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
result_queue.put_nowait((False, exc))
|
|
106
|
+
else:
|
|
107
|
+
result_queue.put_nowait((True, result))
|
|
108
|
+
|
|
109
|
+
thread = threading.Thread(target=run_call, daemon=True)
|
|
110
|
+
thread.start()
|
|
111
|
+
thread.join(timeout=remaining_seconds)
|
|
112
|
+
if thread.is_alive():
|
|
113
|
+
raise _DrainDeadlineExceeded
|
|
114
|
+
|
|
115
|
+
succeeded, value = result_queue.get_nowait()
|
|
116
|
+
if succeeded:
|
|
117
|
+
return cast(_TRedisCall, value)
|
|
118
|
+
raise cast(BaseException, value)
|
|
119
|
+
|
|
120
|
+
|
|
74
121
|
def _coerce_lua_count(value: object) -> int:
|
|
75
122
|
if isinstance(value, bytes):
|
|
76
123
|
value = value.decode("utf-8")
|
|
@@ -80,6 +127,26 @@ def _coerce_lua_count(value: object) -> int:
|
|
|
80
127
|
return 0
|
|
81
128
|
|
|
82
129
|
|
|
130
|
+
def _decode_lua_text(value: object) -> str | None:
|
|
131
|
+
if isinstance(value, bytes):
|
|
132
|
+
value = value.decode("utf-8")
|
|
133
|
+
if isinstance(value, str) and value:
|
|
134
|
+
return value
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _coerce_lua_message_attempts(value: object) -> list[_MessageAttemptEvent]:
|
|
139
|
+
if not isinstance(value, list | tuple):
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
attempts: list[_MessageAttemptEvent] = []
|
|
143
|
+
for item in value:
|
|
144
|
+
if not isinstance(item, list | tuple) or len(item) < 2:
|
|
145
|
+
continue
|
|
146
|
+
attempts.append((_decode_lua_text(item[0]), _coerce_lua_count(item[1])))
|
|
147
|
+
return attempts
|
|
148
|
+
|
|
149
|
+
|
|
83
150
|
def _pending_overload_max_backoff_seconds(block_timeout_seconds: float) -> float:
|
|
84
151
|
return min(_PENDING_OVERLOAD_MAX_BACKOFF_SECONDS, block_timeout_seconds / 10)
|
|
85
152
|
|
|
@@ -214,8 +281,11 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
214
281
|
operation: EventOperation,
|
|
215
282
|
outcome: EventOutcome,
|
|
216
283
|
*,
|
|
284
|
+
message_id: str | None = None,
|
|
217
285
|
claim_id: str | None = None,
|
|
218
286
|
destination_queue: str | None = None,
|
|
287
|
+
delivery_count: int | None = None,
|
|
288
|
+
max_delivery_count: int | None = None,
|
|
219
289
|
exception_type: str | None = None,
|
|
220
290
|
error: BaseException | None = None,
|
|
221
291
|
) -> None:
|
|
@@ -224,8 +294,11 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
224
294
|
self._event_emitter(
|
|
225
295
|
operation,
|
|
226
296
|
outcome,
|
|
297
|
+
message_id=message_id,
|
|
227
298
|
claim_id=claim_id,
|
|
228
299
|
destination_queue=destination_queue,
|
|
300
|
+
delivery_count=delivery_count,
|
|
301
|
+
max_delivery_count=max_delivery_count,
|
|
229
302
|
exception_type=exception_type,
|
|
230
303
|
error=error,
|
|
231
304
|
)
|
|
@@ -233,12 +306,20 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
233
306
|
def _emit_repeated_event(
|
|
234
307
|
self,
|
|
235
308
|
operation: EventOperation,
|
|
236
|
-
|
|
309
|
+
attempts: list[_MessageAttemptEvent],
|
|
237
310
|
*,
|
|
238
311
|
destination_queue: str | None = None,
|
|
312
|
+
max_delivery_count: int | None = None,
|
|
239
313
|
) -> None:
|
|
240
|
-
for
|
|
241
|
-
self._emit_event(
|
|
314
|
+
for message_id, delivery_count in attempts:
|
|
315
|
+
self._emit_event(
|
|
316
|
+
operation,
|
|
317
|
+
"success",
|
|
318
|
+
message_id=message_id,
|
|
319
|
+
destination_queue=destination_queue,
|
|
320
|
+
delivery_count=delivery_count,
|
|
321
|
+
max_delivery_count=max_delivery_count,
|
|
322
|
+
)
|
|
242
323
|
|
|
243
324
|
def _eval(self, *args: object) -> object:
|
|
244
325
|
try:
|
|
@@ -525,6 +606,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
525
606
|
claim_message: Callable[[str, str, str], _TClaim | None],
|
|
526
607
|
non_blocking_retry_log: str,
|
|
527
608
|
polling_retry_log: str,
|
|
609
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
528
610
|
) -> _TClaim | None:
|
|
529
611
|
while True:
|
|
530
612
|
pending_claim_id = self._acquire_pending_claim_id(to_queue)
|
|
@@ -552,7 +634,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
552
634
|
self._emit_event("claim_reclaim", "success", claim_id=pending_claim_id)
|
|
553
635
|
return recovered_claim
|
|
554
636
|
|
|
555
|
-
if self._is_interrupted():
|
|
637
|
+
if self._is_interrupted(is_interrupted):
|
|
556
638
|
return None
|
|
557
639
|
|
|
558
640
|
pending_claim_id_to_share: str | None = None
|
|
@@ -575,7 +657,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
575
657
|
error=exc,
|
|
576
658
|
)
|
|
577
659
|
logger.warning(non_blocking_retry_log, type(exc).__name__)
|
|
578
|
-
if self._is_interrupted():
|
|
660
|
+
if self._is_interrupted(is_interrupted):
|
|
579
661
|
pending_claim_id_to_share = claim_id
|
|
580
662
|
return None
|
|
581
663
|
try:
|
|
@@ -607,7 +689,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
607
689
|
claim_may_need_recovery = False
|
|
608
690
|
last_retryable_exception: Exception | None = None
|
|
609
691
|
while True:
|
|
610
|
-
if self._is_interrupted():
|
|
692
|
+
if self._is_interrupted(is_interrupted):
|
|
611
693
|
if claim_may_need_recovery:
|
|
612
694
|
pending_claim_id_to_share = claim_id
|
|
613
695
|
return None
|
|
@@ -640,7 +722,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
640
722
|
remaining = deadline - time.monotonic()
|
|
641
723
|
if remaining <= 0:
|
|
642
724
|
if last_retryable_exception is not None:
|
|
643
|
-
if self._is_interrupted():
|
|
725
|
+
if self._is_interrupted(is_interrupted):
|
|
644
726
|
if claim_may_need_recovery:
|
|
645
727
|
pending_claim_id_to_share = claim_id
|
|
646
728
|
return None
|
|
@@ -675,11 +757,36 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
675
757
|
def wait_for_message_and_move(self, from_queue: str, to_queue: str) -> ClaimedMessage | MessageData | None:
|
|
676
758
|
if self._is_interrupted():
|
|
677
759
|
return None
|
|
760
|
+
return self._wait_for_message_and_move_interruptible(from_queue, to_queue)
|
|
761
|
+
|
|
762
|
+
def _wait_for_message_and_move_interruptible(
|
|
763
|
+
self,
|
|
764
|
+
from_queue: str,
|
|
765
|
+
to_queue: str,
|
|
766
|
+
*,
|
|
767
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
768
|
+
) -> ClaimedMessage | MessageData | None:
|
|
769
|
+
if self._is_interrupted(is_interrupted):
|
|
770
|
+
return None
|
|
678
771
|
if self._message_visibility_timeout_seconds is not None:
|
|
679
|
-
return self._wait_for_message_with_visibility_timeout(
|
|
680
|
-
|
|
772
|
+
return self._wait_for_message_with_visibility_timeout(
|
|
773
|
+
from_queue,
|
|
774
|
+
to_queue,
|
|
775
|
+
is_interrupted=is_interrupted,
|
|
776
|
+
)
|
|
777
|
+
return self._wait_for_message_without_visibility_timeout(
|
|
778
|
+
from_queue,
|
|
779
|
+
to_queue,
|
|
780
|
+
is_interrupted=is_interrupted,
|
|
781
|
+
)
|
|
681
782
|
|
|
682
|
-
def _wait_for_message_without_visibility_timeout(
|
|
783
|
+
def _wait_for_message_without_visibility_timeout(
|
|
784
|
+
self,
|
|
785
|
+
from_queue: str,
|
|
786
|
+
to_queue: str,
|
|
787
|
+
*,
|
|
788
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
789
|
+
) -> MessageData | None:
|
|
683
790
|
return self._wait_for_claim(
|
|
684
791
|
from_queue,
|
|
685
792
|
to_queue,
|
|
@@ -693,9 +800,16 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
693
800
|
"Transient error during non-visibility-timeout non-blocking claim, retrying once to recover claim: %s"
|
|
694
801
|
),
|
|
695
802
|
polling_retry_log="Transient error during non-visibility-timeout claim poll, will retry: %s",
|
|
803
|
+
is_interrupted=is_interrupted,
|
|
696
804
|
)
|
|
697
805
|
|
|
698
|
-
def _wait_for_message_with_visibility_timeout(
|
|
806
|
+
def _wait_for_message_with_visibility_timeout(
|
|
807
|
+
self,
|
|
808
|
+
from_queue: str,
|
|
809
|
+
to_queue: str,
|
|
810
|
+
*,
|
|
811
|
+
is_interrupted: BaseGracefulInterruptHandler | None = None,
|
|
812
|
+
) -> ClaimedMessage | None:
|
|
699
813
|
return self._wait_for_claim(
|
|
700
814
|
from_queue,
|
|
701
815
|
to_queue,
|
|
@@ -709,6 +823,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
709
823
|
"Transient error during visibility-timeout non-blocking claim, retrying once to recover claim: %s"
|
|
710
824
|
),
|
|
711
825
|
polling_retry_log="Transient error during visibility-timeout claim poll, will retry: %s",
|
|
826
|
+
is_interrupted=is_interrupted,
|
|
712
827
|
)
|
|
713
828
|
|
|
714
829
|
def _claim_message_without_visibility_timeout(
|
|
@@ -760,10 +875,15 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
760
875
|
return None
|
|
761
876
|
|
|
762
877
|
stored_message, lease_token = result[0], result[1]
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
self._emit_repeated_event("claim_reclaim",
|
|
766
|
-
self._emit_repeated_event(
|
|
878
|
+
reclaimed_attempts = _coerce_lua_message_attempts(result[2]) if len(result) > 2 else []
|
|
879
|
+
dead_lettered_attempts = _coerce_lua_message_attempts(result[3]) if len(result) > 3 else []
|
|
880
|
+
self._emit_repeated_event("claim_reclaim", reclaimed_attempts)
|
|
881
|
+
self._emit_repeated_event(
|
|
882
|
+
"dlq",
|
|
883
|
+
dead_lettered_attempts,
|
|
884
|
+
destination_queue=self._dead_letter_queue,
|
|
885
|
+
max_delivery_count=self._max_delivery_count,
|
|
886
|
+
)
|
|
767
887
|
if stored_message in ("", b"") and lease_token in ("", b""):
|
|
768
888
|
return None
|
|
769
889
|
if isinstance(lease_token, bytes):
|
|
@@ -833,16 +953,54 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
833
953
|
def _claim_result_ttl_ms(self) -> str:
|
|
834
954
|
return str(max(self._message_wait_interval_seconds, 120) * 1000)
|
|
835
955
|
|
|
836
|
-
def
|
|
956
|
+
def _cleanup_drained_lease_token_counter(self, processing_queue: str) -> bool:
|
|
957
|
+
if self._message_visibility_timeout_seconds is None:
|
|
958
|
+
return False
|
|
959
|
+
return bool(
|
|
960
|
+
_coerce_lua_count(
|
|
961
|
+
self._eval(
|
|
962
|
+
CLEANUP_DRAINED_LEASE_TOKEN_COUNTER_LUA_SCRIPT,
|
|
963
|
+
5,
|
|
964
|
+
processing_queue,
|
|
965
|
+
self._lease_deadlines_key(processing_queue),
|
|
966
|
+
self._lease_tokens_key(processing_queue),
|
|
967
|
+
self._delivery_counts_key(processing_queue),
|
|
968
|
+
self._lease_token_counter_key(processing_queue),
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def _delete_claim_result_key(
|
|
974
|
+
self,
|
|
975
|
+
claim_result_key: str,
|
|
976
|
+
*,
|
|
977
|
+
deadline_monotonic: float | None = None,
|
|
978
|
+
) -> None:
|
|
837
979
|
try:
|
|
838
|
-
|
|
980
|
+
_call_with_drain_deadline(
|
|
981
|
+
lambda: self._redis_client.delete(claim_result_key),
|
|
982
|
+
deadline_monotonic=deadline_monotonic,
|
|
983
|
+
)
|
|
984
|
+
except _DrainDeadlineExceeded:
|
|
985
|
+
raise
|
|
839
986
|
except Exception:
|
|
840
987
|
# Claim-result keys have bounded TTLs; this cleanup is intentionally best-effort.
|
|
841
988
|
logger.warning("Failed to delete claim result key %s", claim_result_key, exc_info=True)
|
|
842
989
|
|
|
843
|
-
def _delete_claim_result_ref(
|
|
990
|
+
def _delete_claim_result_ref(
|
|
991
|
+
self,
|
|
992
|
+
claim_result_refs_key: str,
|
|
993
|
+
lease_token: str,
|
|
994
|
+
*,
|
|
995
|
+
deadline_monotonic: float | None = None,
|
|
996
|
+
) -> None:
|
|
844
997
|
try:
|
|
845
|
-
|
|
998
|
+
_call_with_drain_deadline(
|
|
999
|
+
lambda: self._redis_client.hdel(claim_result_refs_key, lease_token),
|
|
1000
|
+
deadline_monotonic=deadline_monotonic,
|
|
1001
|
+
)
|
|
1002
|
+
except _DrainDeadlineExceeded:
|
|
1003
|
+
raise
|
|
846
1004
|
except Exception:
|
|
847
1005
|
# Claim-result refs have bounded TTLs; this cleanup is intentionally best-effort.
|
|
848
1006
|
logger.warning(
|
|
@@ -906,25 +1064,48 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
906
1064
|
self,
|
|
907
1065
|
processing_queue: str,
|
|
908
1066
|
claim_id: str,
|
|
1067
|
+
*,
|
|
1068
|
+
deadline_monotonic: float | None = None,
|
|
909
1069
|
) -> MessageData | None:
|
|
1070
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
910
1071
|
claim_result_key = self._claim_result_key(processing_queue, claim_id)
|
|
911
|
-
cached_claim =
|
|
1072
|
+
cached_claim = _call_with_drain_deadline(
|
|
1073
|
+
lambda: self._redis_client.get(claim_result_key),
|
|
1074
|
+
deadline_monotonic=deadline_monotonic,
|
|
1075
|
+
)
|
|
1076
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
912
1077
|
if cached_claim is None:
|
|
913
|
-
cached_claim =
|
|
1078
|
+
cached_claim = _call_with_drain_deadline(
|
|
1079
|
+
lambda: self._redis_client.hget(self._claim_result_ids_key(processing_queue), claim_id),
|
|
1080
|
+
deadline_monotonic=deadline_monotonic,
|
|
1081
|
+
)
|
|
1082
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
914
1083
|
if cached_claim is None:
|
|
915
1084
|
return None
|
|
916
|
-
self._delete_claim_result_key(claim_result_key)
|
|
1085
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
1086
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
917
1087
|
return cached_claim
|
|
918
1088
|
|
|
919
1089
|
def _recover_pending_visibility_timeout_claim(
|
|
920
1090
|
self,
|
|
921
1091
|
processing_queue: str,
|
|
922
1092
|
claim_id: str,
|
|
1093
|
+
*,
|
|
1094
|
+
deadline_monotonic: float | None = None,
|
|
923
1095
|
) -> ClaimedMessage | None:
|
|
1096
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
924
1097
|
claim_result_key = self._claim_result_key(processing_queue, claim_id)
|
|
925
|
-
cached_claim =
|
|
1098
|
+
cached_claim = _call_with_drain_deadline(
|
|
1099
|
+
lambda: self._redis_client.get(claim_result_key),
|
|
1100
|
+
deadline_monotonic=deadline_monotonic,
|
|
1101
|
+
)
|
|
1102
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
926
1103
|
if cached_claim is None:
|
|
927
|
-
cached_claim =
|
|
1104
|
+
cached_claim = _call_with_drain_deadline(
|
|
1105
|
+
lambda: self._redis_client.hget(self._claim_result_ids_key(processing_queue), claim_id),
|
|
1106
|
+
deadline_monotonic=deadline_monotonic,
|
|
1107
|
+
)
|
|
1108
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
928
1109
|
if cached_claim is None:
|
|
929
1110
|
return None
|
|
930
1111
|
|
|
@@ -932,7 +1113,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
932
1113
|
try:
|
|
933
1114
|
claim = json.loads(cached_claim_text)
|
|
934
1115
|
except json.JSONDecodeError:
|
|
935
|
-
self._delete_claim_result_key(claim_result_key)
|
|
1116
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
936
1117
|
return None
|
|
937
1118
|
|
|
938
1119
|
if (
|
|
@@ -941,7 +1122,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
941
1122
|
or not isinstance(claim[0], str)
|
|
942
1123
|
or not isinstance(claim[1], str)
|
|
943
1124
|
):
|
|
944
|
-
self._delete_claim_result_key(claim_result_key)
|
|
1125
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
945
1126
|
return None
|
|
946
1127
|
|
|
947
1128
|
stored_message: MessageData = claim[0]
|
|
@@ -949,12 +1130,19 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
949
1130
|
stored_message = stored_message.encode("utf-8")
|
|
950
1131
|
lease_token = claim[1]
|
|
951
1132
|
|
|
952
|
-
self._delete_claim_result_key(claim_result_key)
|
|
953
|
-
self._delete_claim_result_ref(
|
|
1133
|
+
self._delete_claim_result_key(claim_result_key, deadline_monotonic=deadline_monotonic)
|
|
1134
|
+
self._delete_claim_result_ref(
|
|
1135
|
+
self._claim_result_refs_key(processing_queue),
|
|
1136
|
+
lease_token,
|
|
1137
|
+
deadline_monotonic=deadline_monotonic,
|
|
1138
|
+
)
|
|
1139
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
954
1140
|
return ClaimedMessage(stored_message=stored_message, lease_token=lease_token)
|
|
955
1141
|
|
|
956
|
-
def _is_interrupted(self) -> bool:
|
|
957
|
-
return self._interrupt is not None and self._interrupt.is_interrupted()
|
|
1142
|
+
def _is_interrupted(self, is_interrupted: BaseGracefulInterruptHandler | None = None) -> bool:
|
|
1143
|
+
return (self._interrupt is not None and self._interrupt.is_interrupted()) or (
|
|
1144
|
+
is_interrupted is not None and is_interrupted.is_interrupted()
|
|
1145
|
+
)
|
|
958
1146
|
|
|
959
1147
|
def _drain_pending_claim_ids(
|
|
960
1148
|
self,
|
|
@@ -1010,8 +1198,10 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1010
1198
|
clear = False
|
|
1011
1199
|
try:
|
|
1012
1200
|
try:
|
|
1013
|
-
recover(processing_queue, claim_id)
|
|
1201
|
+
recover(processing_queue, claim_id, deadline_monotonic=deadline_monotonic)
|
|
1014
1202
|
clear = True
|
|
1203
|
+
except _DrainDeadlineExceeded:
|
|
1204
|
+
break
|
|
1015
1205
|
except Exception as exc:
|
|
1016
1206
|
if not is_redis_retryable_exception(exc):
|
|
1017
1207
|
raise
|