redis-message-queue 8.2.2__tar.gz → 8.2.3__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.2 → redis_message_queue-8.2.3}/PKG-INFO +2 -2
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/README.md +1 -1
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/pyproject.toml +2 -2
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_config.py +49 -2
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_redis_gateway.py +86 -11
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/asyncio/_redis_gateway.py +86 -11
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/asyncio/redis_message_queue.py +15 -1
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/redis_message_queue.py +12 -1
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/.gitignore +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/LICENSE +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_event.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_exceptions.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_payload_limits.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/interrupt_handler/_event_driven.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/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.2.
|
|
3
|
+
Version: 8.2.3
|
|
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,7 +34,7 @@ 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.2.
|
|
37
|
+
pip install "redis-message-queue>=8.2.3,<9.0.0"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Requires Redis server >= 6.2.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**Lightweight Python message queuing with Redis and built-in publish-side deduplication.** Deduplicate publishes within a TTL window, with optional crash recovery — across any number of producers and consumers.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
pip install "redis-message-queue>=8.2.
|
|
14
|
+
pip install "redis-message-queue>=8.2.3,<9.0.0"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Requires Redis server >= 6.2.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "redis-message-queue"
|
|
3
|
-
version = "8.2.
|
|
3
|
+
version = "8.2.3"
|
|
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.2.
|
|
51
|
+
current_version = "8.2.3"
|
|
52
52
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
53
53
|
serialize = ["{major}.{minor}.{patch}"]
|
|
54
54
|
search = "{current_version}"
|
|
@@ -585,6 +585,53 @@ return removed
|
|
|
585
585
|
"""
|
|
586
586
|
)
|
|
587
587
|
|
|
588
|
+
RETURN_MESSAGE_TO_PENDING_LUA_SCRIPT = (
|
|
589
|
+
_LUA_KEY_TYPE_GUARD
|
|
590
|
+
+ """
|
|
591
|
+
local err = redis_message_queue_require_type(KEYS[1], 'list')
|
|
592
|
+
if err then
|
|
593
|
+
return err
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
local err = redis_message_queue_require_type(KEYS[2], 'list')
|
|
597
|
+
if err then
|
|
598
|
+
return err
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
local err = redis_message_queue_require_type(KEYS[3], 'hash')
|
|
602
|
+
if err then
|
|
603
|
+
return err
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
local err = redis_message_queue_require_type(KEYS[4], 'hash')
|
|
607
|
+
if err then
|
|
608
|
+
return err
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
local err = redis_message_queue_require_type(KEYS[5], 'string')
|
|
612
|
+
if err then
|
|
613
|
+
return err
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
local cached_result = redis.call('GET', KEYS[5])
|
|
617
|
+
if cached_result then
|
|
618
|
+
return tonumber(cached_result)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
redis.call('RPUSH', KEYS[2], ARGV[1])
|
|
622
|
+
local removed = redis.call('LREM', KEYS[1], 1, ARGV[1])
|
|
623
|
+
if removed == 1 then
|
|
624
|
+
redis.call('HDEL', KEYS[3], ARGV[2])
|
|
625
|
+
redis.call('HDEL', KEYS[4], ARGV[1])
|
|
626
|
+
else
|
|
627
|
+
redis.call('LREM', KEYS[2], 1, ARGV[1])
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
redis.call('SET', KEYS[5], tostring(removed), 'PX', tonumber(ARGV[3]))
|
|
631
|
+
return removed
|
|
632
|
+
"""
|
|
633
|
+
)
|
|
634
|
+
|
|
588
635
|
REMOVE_MESSAGE_LUA_SCRIPT = (
|
|
589
636
|
_LUA_KEY_TYPE_GUARD
|
|
590
637
|
+ """
|
|
@@ -876,8 +923,6 @@ while claim_attempts < 100 do
|
|
|
876
923
|
|
|
877
924
|
local count = redis.call('HINCRBY', KEYS[6], stored, 1)
|
|
878
925
|
if max_delivery_count > 0 and count > max_delivery_count then
|
|
879
|
-
redis.call('LREM', KEYS[2], 1, stored)
|
|
880
|
-
redis.call('HDEL', KEYS[6], stored)
|
|
881
926
|
-- Strip envelope to store raw payload in DLQ, consistent with completed/failed queues.
|
|
882
927
|
-- The per-delivery UUID in the envelope is lost; see README dead-letter notes.
|
|
883
928
|
local dead_letter_value = stored
|
|
@@ -886,6 +931,8 @@ while claim_attempts < 100 do
|
|
|
886
931
|
dead_letter_value = envelope['payload']
|
|
887
932
|
end
|
|
888
933
|
redis.call('LPUSH', KEYS[7], dead_letter_value)
|
|
934
|
+
redis.call('LREM', KEYS[2], 1, stored)
|
|
935
|
+
redis.call('HDEL', KEYS[6], stored)
|
|
889
936
|
table.insert(dead_lettered_events, {redis_message_queue_message_id(stored), tostring(count)})
|
|
890
937
|
else
|
|
891
938
|
return store_claim_and_return(stored)
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -31,6 +31,7 @@ from redis_message_queue._config import (
|
|
|
31
31
|
REMOVE_MESSAGE_LUA_SCRIPT,
|
|
32
32
|
REMOVE_MESSAGE_WITH_LEASE_TOKEN_LUA_SCRIPT,
|
|
33
33
|
RENEW_MESSAGE_LEASE_LUA_SCRIPT,
|
|
34
|
+
RETURN_MESSAGE_TO_PENDING_LUA_SCRIPT,
|
|
34
35
|
_ChainedInterrupt,
|
|
35
36
|
build_retry_strategy,
|
|
36
37
|
is_redis_retryable_exception,
|
|
@@ -79,6 +80,8 @@ _VISIBILITY_TIMEOUT_POLL_INTERVAL_SECONDS = 0.25
|
|
|
79
80
|
_PENDING_OVERLOAD_INITIAL_BACKOFF_SECONDS = 0.010
|
|
80
81
|
_PENDING_OVERLOAD_MAX_BACKOFF_SECONDS = 0.500
|
|
81
82
|
_CLAIM_STORE_FAILED_LUA_SENTINEL_BYTES = CLAIM_STORE_FAILED_LUA_SENTINEL.encode("utf-8")
|
|
83
|
+
_PENDING_QUEUE_SUFFIX = "pending"
|
|
84
|
+
_PROCESSING_QUEUE_SUFFIX = "processing"
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
class _DrainDeadlineExceeded(Exception):
|
|
@@ -1225,6 +1228,53 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1225
1228
|
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1226
1229
|
return ClaimedMessage(stored_message=stored_message, lease_token=lease_token)
|
|
1227
1230
|
|
|
1231
|
+
def _pending_queue_from_processing_queue(self, processing_queue: str) -> str:
|
|
1232
|
+
if not processing_queue.endswith(_PROCESSING_QUEUE_SUFFIX):
|
|
1233
|
+
raise RuntimeError(f"cannot derive pending queue key from processing queue {processing_queue!r}")
|
|
1234
|
+
return f"{processing_queue.removesuffix(_PROCESSING_QUEUE_SUFFIX)}{_PENDING_QUEUE_SUFFIX}"
|
|
1235
|
+
|
|
1236
|
+
def _return_recovered_non_visibility_timeout_claim_to_pending(
|
|
1237
|
+
self,
|
|
1238
|
+
processing_queue: str,
|
|
1239
|
+
stored_message: MessageData,
|
|
1240
|
+
claim_id: str,
|
|
1241
|
+
*,
|
|
1242
|
+
deadline_monotonic: float | None,
|
|
1243
|
+
) -> bool:
|
|
1244
|
+
pending_queue = self._pending_queue_from_processing_queue(processing_queue)
|
|
1245
|
+
operation_id = uuid.uuid4().hex
|
|
1246
|
+
operation_result_key = self._operation_result_key(processing_queue, operation_id)
|
|
1247
|
+
|
|
1248
|
+
try:
|
|
1249
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1250
|
+
result = _call_with_drain_deadline(
|
|
1251
|
+
lambda: self._eval(
|
|
1252
|
+
RETURN_MESSAGE_TO_PENDING_LUA_SCRIPT,
|
|
1253
|
+
5,
|
|
1254
|
+
processing_queue,
|
|
1255
|
+
pending_queue,
|
|
1256
|
+
self._claim_result_ids_key(processing_queue),
|
|
1257
|
+
self._claim_result_backrefs_key(processing_queue),
|
|
1258
|
+
operation_result_key,
|
|
1259
|
+
stored_message,
|
|
1260
|
+
claim_id,
|
|
1261
|
+
self._operation_result_ttl_ms(),
|
|
1262
|
+
),
|
|
1263
|
+
deadline_monotonic=deadline_monotonic,
|
|
1264
|
+
)
|
|
1265
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1266
|
+
return bool(_coerce_lua_count(result))
|
|
1267
|
+
except RedisMessageQueueError as exc:
|
|
1268
|
+
_set_exception_context(
|
|
1269
|
+
exc,
|
|
1270
|
+
queue=processing_queue,
|
|
1271
|
+
message_id=extract_stored_message_id(stored_message),
|
|
1272
|
+
operation="drain",
|
|
1273
|
+
)
|
|
1274
|
+
raise
|
|
1275
|
+
finally:
|
|
1276
|
+
self._delete_operation_result_key(operation_result_key)
|
|
1277
|
+
|
|
1228
1278
|
def _is_interrupted(self, is_interrupted: BaseGracefulInterruptHandler | None = None) -> bool:
|
|
1229
1279
|
return (self._interrupt is not None and self._interrupt.is_interrupted()) or (
|
|
1230
1280
|
is_interrupted is not None and is_interrupted.is_interrupted()
|
|
@@ -1241,8 +1291,10 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1241
1291
|
Walks the same recovery path as ``_wait_for_claim`` but without
|
|
1242
1292
|
gating on the interrupt flag, so a soft shutdown can flush
|
|
1243
1293
|
ambiguous-claim state that would otherwise be dropped on process
|
|
1244
|
-
exit (AA-05-F2).
|
|
1245
|
-
the
|
|
1294
|
+
exit (AA-05-F2). No-visibility-timeout recoveries are returned to
|
|
1295
|
+
the pending queue before their claim ids are cleared. Returns True
|
|
1296
|
+
if no pending ids remain; False if the deadline fired or Redis
|
|
1297
|
+
errors prevented full drain.
|
|
1246
1298
|
"""
|
|
1247
1299
|
with self._drain_pending_claim_ids_lock:
|
|
1248
1300
|
self._last_drain_error = None
|
|
@@ -1258,11 +1310,8 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1258
1310
|
deadline_monotonic: float | None,
|
|
1259
1311
|
) -> bool:
|
|
1260
1312
|
"""Recover every in-memory pending claim id for ``processing_queue``."""
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
else:
|
|
1264
|
-
recover = self._recover_pending_non_visibility_timeout_claim
|
|
1265
|
-
skipped_transient: set[str] = set()
|
|
1313
|
+
has_visibility_timeout = self._message_visibility_timeout_seconds is not None
|
|
1314
|
+
skipped_unresolved: set[str] = set()
|
|
1266
1315
|
last_error: BaseException | None = None
|
|
1267
1316
|
while True:
|
|
1268
1317
|
# ``>=`` (not ``>``) makes ``timeout=0`` deterministically take
|
|
@@ -1278,7 +1327,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1278
1327
|
return True
|
|
1279
1328
|
recovering = self._recovering_claim_ids.setdefault(processing_queue, set())
|
|
1280
1329
|
claim_id = next(
|
|
1281
|
-
(cid for cid in pending if cid not in recovering and cid not in
|
|
1330
|
+
(cid for cid in pending if cid not in recovering and cid not in skipped_unresolved),
|
|
1282
1331
|
None,
|
|
1283
1332
|
)
|
|
1284
1333
|
if claim_id is None:
|
|
@@ -1287,8 +1336,34 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1287
1336
|
clear = False
|
|
1288
1337
|
try:
|
|
1289
1338
|
try:
|
|
1290
|
-
|
|
1291
|
-
|
|
1339
|
+
if has_visibility_timeout:
|
|
1340
|
+
self._recover_pending_visibility_timeout_claim(
|
|
1341
|
+
processing_queue,
|
|
1342
|
+
claim_id,
|
|
1343
|
+
deadline_monotonic=deadline_monotonic,
|
|
1344
|
+
)
|
|
1345
|
+
clear = True
|
|
1346
|
+
continue
|
|
1347
|
+
|
|
1348
|
+
recovered_claim = self._recover_pending_non_visibility_timeout_claim(
|
|
1349
|
+
processing_queue,
|
|
1350
|
+
claim_id,
|
|
1351
|
+
deadline_monotonic=deadline_monotonic,
|
|
1352
|
+
)
|
|
1353
|
+
if recovered_claim is None:
|
|
1354
|
+
clear = True
|
|
1355
|
+
elif self._return_recovered_non_visibility_timeout_claim_to_pending(
|
|
1356
|
+
processing_queue,
|
|
1357
|
+
recovered_claim,
|
|
1358
|
+
claim_id,
|
|
1359
|
+
deadline_monotonic=deadline_monotonic,
|
|
1360
|
+
):
|
|
1361
|
+
clear = True
|
|
1362
|
+
else:
|
|
1363
|
+
last_error = RuntimeError(
|
|
1364
|
+
f"drain recovered claim {claim_id!r} but message was not present in processing queue"
|
|
1365
|
+
)
|
|
1366
|
+
skipped_unresolved.add(claim_id)
|
|
1292
1367
|
except _DrainDeadlineExceeded:
|
|
1293
1368
|
last_error = TimeoutError("drain pending-claim recovery deadline expired")
|
|
1294
1369
|
break
|
|
@@ -1301,7 +1376,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1301
1376
|
claim_id,
|
|
1302
1377
|
type(exc).__name__,
|
|
1303
1378
|
)
|
|
1304
|
-
|
|
1379
|
+
skipped_unresolved.add(claim_id)
|
|
1305
1380
|
finally:
|
|
1306
1381
|
self._finish_pending_claim_recovery(processing_queue, claim_id, clear=clear)
|
|
1307
1382
|
with self._pending_claim_ids_lock:
|
|
@@ -29,6 +29,7 @@ from redis_message_queue._config import (
|
|
|
29
29
|
REMOVE_MESSAGE_LUA_SCRIPT,
|
|
30
30
|
REMOVE_MESSAGE_WITH_LEASE_TOKEN_LUA_SCRIPT,
|
|
31
31
|
RENEW_MESSAGE_LEASE_LUA_SCRIPT,
|
|
32
|
+
RETURN_MESSAGE_TO_PENDING_LUA_SCRIPT,
|
|
32
33
|
_ChainedInterrupt,
|
|
33
34
|
build_retry_strategy,
|
|
34
35
|
is_redis_retryable_exception,
|
|
@@ -78,6 +79,8 @@ _VISIBILITY_TIMEOUT_POLL_INTERVAL_SECONDS = 0.25
|
|
|
78
79
|
_PENDING_OVERLOAD_INITIAL_BACKOFF_SECONDS = 0.010
|
|
79
80
|
_PENDING_OVERLOAD_MAX_BACKOFF_SECONDS = 0.500
|
|
80
81
|
_CLAIM_STORE_FAILED_LUA_SENTINEL_BYTES = CLAIM_STORE_FAILED_LUA_SENTINEL.encode("utf-8")
|
|
82
|
+
_PENDING_QUEUE_SUFFIX = "pending"
|
|
83
|
+
_PROCESSING_QUEUE_SUFFIX = "processing"
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
class _DrainDeadlineExceeded(Exception):
|
|
@@ -1206,6 +1209,53 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1206
1209
|
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1207
1210
|
return ClaimedMessage(stored_message=stored_message, lease_token=lease_token)
|
|
1208
1211
|
|
|
1212
|
+
def _pending_queue_from_processing_queue(self, processing_queue: str) -> str:
|
|
1213
|
+
if not processing_queue.endswith(_PROCESSING_QUEUE_SUFFIX):
|
|
1214
|
+
raise RuntimeError(f"cannot derive pending queue key from processing queue {processing_queue!r}")
|
|
1215
|
+
return f"{processing_queue.removesuffix(_PROCESSING_QUEUE_SUFFIX)}{_PENDING_QUEUE_SUFFIX}"
|
|
1216
|
+
|
|
1217
|
+
async def _return_recovered_non_visibility_timeout_claim_to_pending(
|
|
1218
|
+
self,
|
|
1219
|
+
processing_queue: str,
|
|
1220
|
+
stored_message: MessageData,
|
|
1221
|
+
claim_id: str,
|
|
1222
|
+
*,
|
|
1223
|
+
deadline_monotonic: float | None,
|
|
1224
|
+
) -> bool:
|
|
1225
|
+
pending_queue = self._pending_queue_from_processing_queue(processing_queue)
|
|
1226
|
+
operation_id = uuid.uuid4().hex
|
|
1227
|
+
operation_result_key = self._operation_result_key(processing_queue, operation_id)
|
|
1228
|
+
|
|
1229
|
+
try:
|
|
1230
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1231
|
+
result = await _call_with_drain_deadline(
|
|
1232
|
+
lambda: self._eval(
|
|
1233
|
+
RETURN_MESSAGE_TO_PENDING_LUA_SCRIPT,
|
|
1234
|
+
5,
|
|
1235
|
+
processing_queue,
|
|
1236
|
+
pending_queue,
|
|
1237
|
+
self._claim_result_ids_key(processing_queue),
|
|
1238
|
+
self._claim_result_backrefs_key(processing_queue),
|
|
1239
|
+
operation_result_key,
|
|
1240
|
+
stored_message,
|
|
1241
|
+
claim_id,
|
|
1242
|
+
self._operation_result_ttl_ms(),
|
|
1243
|
+
),
|
|
1244
|
+
deadline_monotonic=deadline_monotonic,
|
|
1245
|
+
)
|
|
1246
|
+
_raise_if_drain_deadline_expired(deadline_monotonic)
|
|
1247
|
+
return bool(_coerce_lua_count(result))
|
|
1248
|
+
except RedisMessageQueueError as exc:
|
|
1249
|
+
_set_exception_context(
|
|
1250
|
+
exc,
|
|
1251
|
+
queue=processing_queue,
|
|
1252
|
+
message_id=extract_stored_message_id(stored_message),
|
|
1253
|
+
operation="drain",
|
|
1254
|
+
)
|
|
1255
|
+
raise
|
|
1256
|
+
finally:
|
|
1257
|
+
await self._delete_operation_result_key(operation_result_key)
|
|
1258
|
+
|
|
1209
1259
|
def _is_interrupted(self, is_interrupted: BaseGracefulInterruptHandler | None = None) -> bool:
|
|
1210
1260
|
return (self._interrupt is not None and self._interrupt.is_interrupted()) or (
|
|
1211
1261
|
is_interrupted is not None and is_interrupted.is_interrupted()
|
|
@@ -1222,8 +1272,10 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1222
1272
|
Kept separate from the sync implementation per AF11 (sync/async
|
|
1223
1273
|
gateways stay duplicated). Walks the same recovery path as
|
|
1224
1274
|
``_wait_for_claim`` but without gating on the interrupt flag so a
|
|
1225
|
-
soft shutdown can flush ambiguous-claim state.
|
|
1226
|
-
|
|
1275
|
+
soft shutdown can flush ambiguous-claim state. No-visibility-timeout
|
|
1276
|
+
recoveries are returned to the pending queue before their claim ids
|
|
1277
|
+
are cleared. Returns True if no pending ids remain; False on deadline
|
|
1278
|
+
expiry or Redis errors.
|
|
1227
1279
|
"""
|
|
1228
1280
|
async with self._drain_pending_claim_ids_lock:
|
|
1229
1281
|
self._last_drain_error = None
|
|
@@ -1239,11 +1291,8 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1239
1291
|
deadline_monotonic: float | None,
|
|
1240
1292
|
) -> bool:
|
|
1241
1293
|
"""Recover every in-memory pending claim id for ``processing_queue``."""
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
else:
|
|
1245
|
-
recover = self._recover_pending_non_visibility_timeout_claim
|
|
1246
|
-
skipped_transient: set[str] = set()
|
|
1294
|
+
has_visibility_timeout = self._message_visibility_timeout_seconds is not None
|
|
1295
|
+
skipped_unresolved: set[str] = set()
|
|
1247
1296
|
loop = asyncio.get_running_loop()
|
|
1248
1297
|
last_error: BaseException | None = None
|
|
1249
1298
|
while True:
|
|
@@ -1258,7 +1307,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1258
1307
|
return True
|
|
1259
1308
|
recovering = self._recovering_claim_ids.setdefault(processing_queue, set())
|
|
1260
1309
|
claim_id = next(
|
|
1261
|
-
(cid for cid in pending if cid not in recovering and cid not in
|
|
1310
|
+
(cid for cid in pending if cid not in recovering and cid not in skipped_unresolved),
|
|
1262
1311
|
None,
|
|
1263
1312
|
)
|
|
1264
1313
|
if claim_id is None:
|
|
@@ -1267,8 +1316,34 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1267
1316
|
clear = False
|
|
1268
1317
|
try:
|
|
1269
1318
|
try:
|
|
1270
|
-
|
|
1271
|
-
|
|
1319
|
+
if has_visibility_timeout:
|
|
1320
|
+
await self._recover_pending_visibility_timeout_claim(
|
|
1321
|
+
processing_queue,
|
|
1322
|
+
claim_id,
|
|
1323
|
+
deadline_monotonic=deadline_monotonic,
|
|
1324
|
+
)
|
|
1325
|
+
clear = True
|
|
1326
|
+
continue
|
|
1327
|
+
|
|
1328
|
+
recovered_claim = await self._recover_pending_non_visibility_timeout_claim(
|
|
1329
|
+
processing_queue,
|
|
1330
|
+
claim_id,
|
|
1331
|
+
deadline_monotonic=deadline_monotonic,
|
|
1332
|
+
)
|
|
1333
|
+
if recovered_claim is None:
|
|
1334
|
+
clear = True
|
|
1335
|
+
elif await self._return_recovered_non_visibility_timeout_claim_to_pending(
|
|
1336
|
+
processing_queue,
|
|
1337
|
+
recovered_claim,
|
|
1338
|
+
claim_id,
|
|
1339
|
+
deadline_monotonic=deadline_monotonic,
|
|
1340
|
+
):
|
|
1341
|
+
clear = True
|
|
1342
|
+
else:
|
|
1343
|
+
last_error = RuntimeError(
|
|
1344
|
+
f"drain recovered claim {claim_id!r} but message was not present in processing queue"
|
|
1345
|
+
)
|
|
1346
|
+
skipped_unresolved.add(claim_id)
|
|
1272
1347
|
except _DrainDeadlineExceeded:
|
|
1273
1348
|
last_error = TimeoutError("drain pending-claim recovery deadline expired")
|
|
1274
1349
|
break
|
|
@@ -1281,7 +1356,7 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
1281
1356
|
claim_id,
|
|
1282
1357
|
type(exc).__name__,
|
|
1283
1358
|
)
|
|
1284
|
-
|
|
1359
|
+
skipped_unresolved.add(claim_id)
|
|
1285
1360
|
finally:
|
|
1286
1361
|
self._finish_pending_claim_recovery(processing_queue, claim_id, clear=clear)
|
|
1287
1362
|
with self._pending_claim_ids_lock:
|
|
@@ -920,6 +920,18 @@ class RedisMessageQueue:
|
|
|
920
920
|
"Use an async callable or return an awaitable."
|
|
921
921
|
)
|
|
922
922
|
await result
|
|
923
|
+
except asyncio.CancelledError as exc:
|
|
924
|
+
current_task = asyncio.current_task()
|
|
925
|
+
if current_task is not None and current_task.cancelling() > 0:
|
|
926
|
+
raise
|
|
927
|
+
logger.exception("on_event callback raised an exception")
|
|
928
|
+
with warnings.catch_warnings():
|
|
929
|
+
warnings.simplefilter("always", RuntimeWarning)
|
|
930
|
+
warnings.warn(
|
|
931
|
+
f"on_event callback raised {type(exc).__name__}",
|
|
932
|
+
RuntimeWarning,
|
|
933
|
+
stacklevel=2,
|
|
934
|
+
)
|
|
923
935
|
except Exception as exc:
|
|
924
936
|
logger.exception("on_event callback raised an exception")
|
|
925
937
|
with warnings.catch_warnings():
|
|
@@ -1414,7 +1426,9 @@ class RedisMessageQueue:
|
|
|
1414
1426
|
calls raise ``QueueDrainedError``. It then awaits the gateway's
|
|
1415
1427
|
pending-claim-id recovery loop. Returns ``True`` if all pending claim
|
|
1416
1428
|
ids were recovered, ``False`` if the deadline fired or a transient
|
|
1417
|
-
Redis error left ids pending.
|
|
1429
|
+
Redis error left ids pending. In no-visibility-timeout queues,
|
|
1430
|
+
recovered messages are returned to pending before the claim id is
|
|
1431
|
+
cleared.
|
|
1418
1432
|
|
|
1419
1433
|
Unlike ``asyncio.CancelledError`` (hard-abort, leaves messages
|
|
1420
1434
|
claimed for VT-reclaim), ``aclose()`` is the explicit-drain
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import hashlib
|
|
2
3
|
import inspect
|
|
3
4
|
import logging
|
|
@@ -883,6 +884,15 @@ class RedisMessageQueue:
|
|
|
883
884
|
"'on_event' returned an awaitable; use the async RedisMessageQueue "
|
|
884
885
|
"from redis_message_queue.asyncio instead"
|
|
885
886
|
)
|
|
887
|
+
except asyncio.CancelledError as exc:
|
|
888
|
+
logger.exception("on_event callback raised an exception")
|
|
889
|
+
with warnings.catch_warnings():
|
|
890
|
+
warnings.simplefilter("always", RuntimeWarning)
|
|
891
|
+
warnings.warn(
|
|
892
|
+
f"on_event callback raised {type(exc).__name__}",
|
|
893
|
+
RuntimeWarning,
|
|
894
|
+
stacklevel=2,
|
|
895
|
+
)
|
|
886
896
|
except Exception as exc:
|
|
887
897
|
logger.exception("on_event callback raised an exception")
|
|
888
898
|
with warnings.catch_warnings():
|
|
@@ -1374,7 +1384,8 @@ class RedisMessageQueue:
|
|
|
1374
1384
|
calls raise ``QueueDrainedError``. It then walks the gateway's
|
|
1375
1385
|
in-memory ``_pending_claim_ids`` to recover any ambiguous claims that
|
|
1376
1386
|
an interrupt-aware shutdown would otherwise drop on process exit
|
|
1377
|
-
(AA-05-F2).
|
|
1387
|
+
(AA-05-F2). In no-visibility-timeout queues, recovered messages are
|
|
1388
|
+
returned to pending before the claim id is cleared.
|
|
1378
1389
|
|
|
1379
1390
|
``timeout`` bounds the pending-claim-id recovery loop in seconds;
|
|
1380
1391
|
``None`` waits indefinitely, ``0`` skips the loop entirely. The
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_payload_limits.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-8.2.2 → redis_message_queue-8.2.3}/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
|