redis-message-queue 6.0.0__tar.gz → 6.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-6.0.0 → redis_message_queue-6.0.1}/PKG-INFO +40 -2
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/README.md +39 -1
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/pyproject.toml +1 -1
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_config.py +11 -1
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_redis_gateway.py +15 -5
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/asyncio/_redis_gateway.py +15 -5
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/asyncio/redis_message_queue.py +23 -9
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/redis_message_queue.py +29 -8
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/LICENSE +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/__init__.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_callable_utils.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_event.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_exceptions.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_queue_key_manager.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_redis_cluster.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_stored_message.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/asyncio/__init__.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/interrupt_handler/__init__.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/interrupt_handler/_interface.py +0 -0
- {redis_message_queue-6.0.0 → redis_message_queue-6.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: 6.0.
|
|
3
|
+
Version: 6.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)
|
|
@@ -587,6 +587,44 @@ For a full analysis, see [docs/production-readiness.md](docs/production-readines
|
|
|
587
587
|
- **Do not switch sync and async gateway instances mid-process while claims are active.** Redis state is compatible across deploys, but each gateway instance keeps its own pending claim-recovery IDs. In-flight claim recovery state does not transfer between instances.
|
|
588
588
|
- **Switching between `gateway=` and `client=` can retarget the DLQ.** The built-in `client=` path derives the DLQ from the queue name. If a custom gateway used a different `dead_letter_queue`, switching paths has the same orphaning impact as renaming the DLQ.
|
|
589
589
|
|
|
590
|
+
### v5 to v6 migration
|
|
591
|
+
|
|
592
|
+
v6.0.0 is a non-breaking-defaults release that adds new public APIs. v5 code continues to work; v6 adds opt-in features.
|
|
593
|
+
|
|
594
|
+
**New APIs (opt in as needed):**
|
|
595
|
+
|
|
596
|
+
- `max_pending_length=N` caps pending-list depth; with `pending_overload_policy="raise"` (default) producers see `QueueBackpressureError` when the cap is hit; `"block"` waits up to `pending_overload_block_timeout_seconds`; `"drop_oldest"` evicts silently, so use it only when data loss is acceptable.
|
|
597
|
+
- `queue.drain(timeout=...)` (sync) and `await queue.aclose(timeout=...)` (async) are explicit graceful-shutdown hooks. They refuse new claims and recover pending claim IDs but do not cancel in-flight handlers; join or await your worker separately.
|
|
598
|
+
- `on_event=callback` receives a `QueueEvent` dataclass for every publish/claim/ack/reclaim/dedup/cleanup lifecycle event. Use it for metrics, tracing, and structured logging. See [`examples/production/observability.py`](examples/production/observability.py) for the adapter pattern.
|
|
599
|
+
|
|
600
|
+
**New constructor rejections:**
|
|
601
|
+
|
|
602
|
+
- Passing a `redis.sentinel.Sentinel` manager object now raises at construction. Use `sentinel.master_for(name)` instead.
|
|
603
|
+
- Passing `redis.Redis(single_connection_client=True)` to the sync built-in gateway now raises. Use a normal pooled `redis.Redis` client.
|
|
604
|
+
|
|
605
|
+
**Custom gateway migration:**
|
|
606
|
+
|
|
607
|
+
If you subclass `AbstractRedisGateway` and override `renew_message_lease`, add a keyword argument:
|
|
608
|
+
|
|
609
|
+
```python
|
|
610
|
+
def renew_message_lease(
|
|
611
|
+
self,
|
|
612
|
+
queue,
|
|
613
|
+
message,
|
|
614
|
+
lease_token,
|
|
615
|
+
*,
|
|
616
|
+
is_interrupted=None, # NEW in v6: heartbeat passes a stop-signal observer
|
|
617
|
+
) -> bool:
|
|
618
|
+
...
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Async gateways need the same signature on `async def`. Honor `is_interrupted.is_interrupted()` in your retry loops to stop renewing when the queue is shutting down.
|
|
622
|
+
|
|
623
|
+
**Exception handling:**
|
|
624
|
+
|
|
625
|
+
- Catch `QueueBackpressureError` around publish if you opt into backpressure.
|
|
626
|
+
- A new base class `RedisMessageQueueError` lets you catch all library-owned exceptions in one place. Existing catches for `ValueError`, `TypeError`, `redis.RedisError`, etc. continue to work because the new subclasses preserve those bases.
|
|
627
|
+
|
|
590
628
|
### v2 to v3 migration
|
|
591
629
|
|
|
592
630
|
v3.0.0 replaced the `retry_strategy: Callable` constructor parameter with `retry_budget_seconds`, `retry_max_delay_seconds`, and `retry_initial_delay_seconds`. Users with custom retry strategies should subclass `AbstractRedisGateway` instead (see [Custom gateway](#custom-gateway)).
|
|
@@ -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)
|
|
@@ -561,6 +561,44 @@ For a full analysis, see [docs/production-readiness.md](docs/production-readines
|
|
|
561
561
|
- **Do not switch sync and async gateway instances mid-process while claims are active.** Redis state is compatible across deploys, but each gateway instance keeps its own pending claim-recovery IDs. In-flight claim recovery state does not transfer between instances.
|
|
562
562
|
- **Switching between `gateway=` and `client=` can retarget the DLQ.** The built-in `client=` path derives the DLQ from the queue name. If a custom gateway used a different `dead_letter_queue`, switching paths has the same orphaning impact as renaming the DLQ.
|
|
563
563
|
|
|
564
|
+
### v5 to v6 migration
|
|
565
|
+
|
|
566
|
+
v6.0.0 is a non-breaking-defaults release that adds new public APIs. v5 code continues to work; v6 adds opt-in features.
|
|
567
|
+
|
|
568
|
+
**New APIs (opt in as needed):**
|
|
569
|
+
|
|
570
|
+
- `max_pending_length=N` caps pending-list depth; with `pending_overload_policy="raise"` (default) producers see `QueueBackpressureError` when the cap is hit; `"block"` waits up to `pending_overload_block_timeout_seconds`; `"drop_oldest"` evicts silently, so use it only when data loss is acceptable.
|
|
571
|
+
- `queue.drain(timeout=...)` (sync) and `await queue.aclose(timeout=...)` (async) are explicit graceful-shutdown hooks. They refuse new claims and recover pending claim IDs but do not cancel in-flight handlers; join or await your worker separately.
|
|
572
|
+
- `on_event=callback` receives a `QueueEvent` dataclass for every publish/claim/ack/reclaim/dedup/cleanup lifecycle event. Use it for metrics, tracing, and structured logging. See [`examples/production/observability.py`](examples/production/observability.py) for the adapter pattern.
|
|
573
|
+
|
|
574
|
+
**New constructor rejections:**
|
|
575
|
+
|
|
576
|
+
- Passing a `redis.sentinel.Sentinel` manager object now raises at construction. Use `sentinel.master_for(name)` instead.
|
|
577
|
+
- Passing `redis.Redis(single_connection_client=True)` to the sync built-in gateway now raises. Use a normal pooled `redis.Redis` client.
|
|
578
|
+
|
|
579
|
+
**Custom gateway migration:**
|
|
580
|
+
|
|
581
|
+
If you subclass `AbstractRedisGateway` and override `renew_message_lease`, add a keyword argument:
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
def renew_message_lease(
|
|
585
|
+
self,
|
|
586
|
+
queue,
|
|
587
|
+
message,
|
|
588
|
+
lease_token,
|
|
589
|
+
*,
|
|
590
|
+
is_interrupted=None, # NEW in v6: heartbeat passes a stop-signal observer
|
|
591
|
+
) -> bool:
|
|
592
|
+
...
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Async gateways need the same signature on `async def`. Honor `is_interrupted.is_interrupted()` in your retry loops to stop renewing when the queue is shutting down.
|
|
596
|
+
|
|
597
|
+
**Exception handling:**
|
|
598
|
+
|
|
599
|
+
- Catch `QueueBackpressureError` around publish if you opt into backpressure.
|
|
600
|
+
- A new base class `RedisMessageQueueError` lets you catch all library-owned exceptions in one place. Existing catches for `ValueError`, `TypeError`, `redis.RedisError`, etc. continue to work because the new subclasses preserve those bases.
|
|
601
|
+
|
|
564
602
|
### v2 to v3 migration
|
|
565
603
|
|
|
566
604
|
v3.0.0 replaced the `retry_strategy: Callable` constructor parameter with `retry_budget_seconds`, `retry_max_delay_seconds`, and `retry_initial_delay_seconds`. Users with custom retry strategies should subclass `AbstractRedisGateway` instead (see [Custom gateway](#custom-gateway)).
|
|
@@ -14,7 +14,7 @@ from tenacity import (
|
|
|
14
14
|
wait_exponential_jitter,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from redis_message_queue._exceptions import ConfigurationError
|
|
17
|
+
from redis_message_queue._exceptions import ConfigurationError, RetryBudgetExhaustedError
|
|
18
18
|
from redis_message_queue.interrupt_handler._interface import (
|
|
19
19
|
BaseGracefulInterruptHandler,
|
|
20
20
|
)
|
|
@@ -91,6 +91,15 @@ def _noop_retry(func):
|
|
|
91
91
|
return func
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _raise_retry_budget_exhausted(retry_state: RetryCallState) -> typing.NoReturn:
|
|
95
|
+
exc = retry_state.outcome.exception() if retry_state.outcome is not None else None
|
|
96
|
+
if exc is None:
|
|
97
|
+
raise RetryBudgetExhaustedError("Redis retry budget exhausted")
|
|
98
|
+
raise RetryBudgetExhaustedError(
|
|
99
|
+
f"Redis retry budget exhausted after {retry_state.attempt_number} attempts"
|
|
100
|
+
) from exc
|
|
101
|
+
|
|
102
|
+
|
|
94
103
|
def build_retry_strategy(
|
|
95
104
|
*,
|
|
96
105
|
retry_budget_seconds: int,
|
|
@@ -113,6 +122,7 @@ def build_retry_strategy(
|
|
|
113
122
|
get_parent_retry=lambda: retry_if_exception(is_redis_retryable_exception),
|
|
114
123
|
),
|
|
115
124
|
after=after_log(logger, logging.WARNING),
|
|
125
|
+
retry_error_callback=_raise_retry_budget_exhausted,
|
|
116
126
|
reraise=True,
|
|
117
127
|
)
|
|
118
128
|
|
{redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_redis_gateway.py
RENAMED
|
@@ -35,7 +35,11 @@ from redis_message_queue._config import (
|
|
|
35
35
|
validate_pending_backpressure_parameters,
|
|
36
36
|
)
|
|
37
37
|
from redis_message_queue._event import EventOperation, EventOutcome
|
|
38
|
-
from redis_message_queue._exceptions import
|
|
38
|
+
from redis_message_queue._exceptions import (
|
|
39
|
+
QueueBackpressureError,
|
|
40
|
+
RetryBudgetExhaustedError,
|
|
41
|
+
wrap_lua_response_error,
|
|
42
|
+
)
|
|
39
43
|
from redis_message_queue._stored_message import (
|
|
40
44
|
ClaimedMessage,
|
|
41
45
|
MessageData,
|
|
@@ -555,7 +559,9 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
555
559
|
claim_id=claim_id,
|
|
556
560
|
exception_type=type(retry_exc).__name__,
|
|
557
561
|
)
|
|
558
|
-
raise
|
|
562
|
+
raise RetryBudgetExhaustedError(
|
|
563
|
+
"Redis retry budget exhausted during message claim"
|
|
564
|
+
) from retry_exc
|
|
559
565
|
except BaseException:
|
|
560
566
|
pending_claim_id_to_share = claim_id
|
|
561
567
|
raise
|
|
@@ -624,7 +630,9 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
624
630
|
claim_id=claim_id,
|
|
625
631
|
exception_type=type(last_retryable_exception).__name__,
|
|
626
632
|
)
|
|
627
|
-
raise
|
|
633
|
+
raise RetryBudgetExhaustedError(
|
|
634
|
+
"Redis retry budget exhausted during message claim"
|
|
635
|
+
) from last_retryable_exception
|
|
628
636
|
return None
|
|
629
637
|
time.sleep(min(_VISIBILITY_TIMEOUT_POLL_INTERVAL_SECONDS, remaining))
|
|
630
638
|
finally:
|
|
@@ -796,13 +804,15 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
796
804
|
try:
|
|
797
805
|
self._redis_client.delete(claim_result_key)
|
|
798
806
|
except Exception:
|
|
799
|
-
|
|
807
|
+
# Claim-result keys have bounded TTLs; this cleanup is intentionally best-effort.
|
|
808
|
+
logger.warning("Failed to delete claim result key %s", claim_result_key, exc_info=True)
|
|
800
809
|
|
|
801
810
|
def _delete_claim_result_ref(self, claim_result_refs_key: str, lease_token: str) -> None:
|
|
802
811
|
try:
|
|
803
812
|
self._redis_client.hdel(claim_result_refs_key, lease_token)
|
|
804
813
|
except Exception:
|
|
805
|
-
|
|
814
|
+
# Claim-result refs have bounded TTLs; this cleanup is intentionally best-effort.
|
|
815
|
+
logger.warning(
|
|
806
816
|
"Failed to delete claim result reference %s[%s]",
|
|
807
817
|
claim_result_refs_key,
|
|
808
818
|
lease_token,
|
|
@@ -34,7 +34,11 @@ from redis_message_queue._config import (
|
|
|
34
34
|
validate_pending_backpressure_parameters,
|
|
35
35
|
)
|
|
36
36
|
from redis_message_queue._event import EventOperation, EventOutcome
|
|
37
|
-
from redis_message_queue._exceptions import
|
|
37
|
+
from redis_message_queue._exceptions import (
|
|
38
|
+
QueueBackpressureError,
|
|
39
|
+
RetryBudgetExhaustedError,
|
|
40
|
+
wrap_lua_response_error,
|
|
41
|
+
)
|
|
38
42
|
from redis_message_queue._stored_message import (
|
|
39
43
|
ClaimedMessage,
|
|
40
44
|
MessageData,
|
|
@@ -551,7 +555,9 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
551
555
|
claim_id=claim_id,
|
|
552
556
|
exception_type=type(retry_exc).__name__,
|
|
553
557
|
)
|
|
554
|
-
raise
|
|
558
|
+
raise RetryBudgetExhaustedError(
|
|
559
|
+
"Redis retry budget exhausted during message claim"
|
|
560
|
+
) from retry_exc
|
|
555
561
|
except BaseException:
|
|
556
562
|
pending_claim_id_to_share = claim_id
|
|
557
563
|
raise
|
|
@@ -621,7 +627,9 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
621
627
|
claim_id=claim_id,
|
|
622
628
|
exception_type=type(last_retryable_exception).__name__,
|
|
623
629
|
)
|
|
624
|
-
raise
|
|
630
|
+
raise RetryBudgetExhaustedError(
|
|
631
|
+
"Redis retry budget exhausted during message claim"
|
|
632
|
+
) from last_retryable_exception
|
|
625
633
|
return None
|
|
626
634
|
await asyncio.sleep(min(_VISIBILITY_TIMEOUT_POLL_INTERVAL_SECONDS, remaining))
|
|
627
635
|
finally:
|
|
@@ -793,13 +801,15 @@ class RedisGateway(AbstractRedisGateway):
|
|
|
793
801
|
try:
|
|
794
802
|
await self._redis_client.delete(claim_result_key)
|
|
795
803
|
except Exception:
|
|
796
|
-
|
|
804
|
+
# Claim-result keys have bounded TTLs; this cleanup is intentionally best-effort.
|
|
805
|
+
logger.warning("Failed to delete claim result key %s", claim_result_key, exc_info=True)
|
|
797
806
|
|
|
798
807
|
async def _delete_claim_result_ref(self, claim_result_refs_key: str, lease_token: str) -> None:
|
|
799
808
|
try:
|
|
800
809
|
await self._redis_client.hdel(claim_result_refs_key, lease_token)
|
|
801
810
|
except Exception:
|
|
802
|
-
|
|
811
|
+
# Claim-result refs have bounded TTLs; this cleanup is intentionally best-effort.
|
|
812
|
+
logger.warning(
|
|
803
813
|
"Failed to delete claim result reference %s[%s]",
|
|
804
814
|
claim_result_refs_key,
|
|
805
815
|
lease_token,
|
|
@@ -18,7 +18,7 @@ from redis_message_queue._config import (
|
|
|
18
18
|
validate_pending_backpressure_parameters,
|
|
19
19
|
)
|
|
20
20
|
from redis_message_queue._event import EventOperation, EventOutcome, QueueEvent
|
|
21
|
-
from redis_message_queue._exceptions import ConfigurationError, GatewayContractError
|
|
21
|
+
from redis_message_queue._exceptions import CleanupFailedError, ConfigurationError, GatewayContractError
|
|
22
22
|
from redis_message_queue._queue_key_manager import QueueKeyManager
|
|
23
23
|
from redis_message_queue._redis_cluster import validate_queue_keys_for_redis_cluster
|
|
24
24
|
from redis_message_queue._stored_message import (
|
|
@@ -573,6 +573,17 @@ class RedisMessageQueue:
|
|
|
573
573
|
" Expected a function that takes the message (str | dict) and returns a str (or an awaitable thereof)."
|
|
574
574
|
" Example: get_deduplication_key=lambda msg: msg['user_id']"
|
|
575
575
|
)
|
|
576
|
+
if (
|
|
577
|
+
max_pending_length is not None
|
|
578
|
+
and pending_overload_policy == "drop_oldest"
|
|
579
|
+
and (deduplication or get_deduplication_key_was_configured)
|
|
580
|
+
):
|
|
581
|
+
raise ConfigurationError(
|
|
582
|
+
"'pending_overload_policy=drop_oldest' cannot be used with deduplication because dropped messages "
|
|
583
|
+
"leave their deduplication keys in Redis, causing future publishes of the same payload to be "
|
|
584
|
+
"silently suppressed. Use 'raise' or 'block' for deduplicated queues, or disable deduplication if "
|
|
585
|
+
"'drop_oldest' is required."
|
|
586
|
+
)
|
|
576
587
|
if not deduplication and get_deduplication_key_was_configured:
|
|
577
588
|
raise ConfigurationError("'get_deduplication_key' cannot be provided when 'deduplication' is disabled.")
|
|
578
589
|
if on_heartbeat_failure is not None and not callable(on_heartbeat_failure):
|
|
@@ -725,11 +736,13 @@ class RedisMessageQueue:
|
|
|
725
736
|
await result
|
|
726
737
|
except Exception as exc:
|
|
727
738
|
logger.exception("on_event callback raised an exception")
|
|
728
|
-
warnings.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
739
|
+
with warnings.catch_warnings():
|
|
740
|
+
warnings.simplefilter("always", RuntimeWarning)
|
|
741
|
+
warnings.warn(
|
|
742
|
+
f"on_event callback raised {type(exc).__name__}",
|
|
743
|
+
RuntimeWarning,
|
|
744
|
+
stacklevel=2,
|
|
745
|
+
)
|
|
733
746
|
|
|
734
747
|
async def publish(self, message: str | dict) -> bool:
|
|
735
748
|
"""Publish a message.
|
|
@@ -926,6 +939,7 @@ class RedisMessageQueue:
|
|
|
926
939
|
)
|
|
927
940
|
warnings.warn(_STALE_LEASE_NACK_WARNING, RuntimeWarning, stacklevel=2)
|
|
928
941
|
except BaseException as cleanup_exc:
|
|
942
|
+
# The handler exception is the user-visible failure; cleanup failure is secondary.
|
|
929
943
|
logger.exception("Failed to clean up message from processing queue")
|
|
930
944
|
await self._emit_event(
|
|
931
945
|
"cleanup_failed",
|
|
@@ -952,7 +966,7 @@ class RedisMessageQueue:
|
|
|
952
966
|
cleanup_operation = self._remove_processed_message(stored_message, lease_token)
|
|
953
967
|
try:
|
|
954
968
|
applied = await _await_preserving_cancellation(cleanup_operation)
|
|
955
|
-
except
|
|
969
|
+
except Exception as cleanup_exc:
|
|
956
970
|
await self._emit_event(
|
|
957
971
|
"cleanup_failed",
|
|
958
972
|
"failure",
|
|
@@ -961,7 +975,7 @@ class RedisMessageQueue:
|
|
|
961
975
|
exception_type=type(cleanup_exc).__name__,
|
|
962
976
|
duration_ms=_duration_ms(cleanup_started_at),
|
|
963
977
|
)
|
|
964
|
-
raise
|
|
978
|
+
raise CleanupFailedError("Cleanup after successful processing failed") from cleanup_exc
|
|
965
979
|
if self._enable_completed_queue:
|
|
966
980
|
await self._emit_event(
|
|
967
981
|
"completed",
|
|
@@ -1083,7 +1097,7 @@ class RedisMessageQueue:
|
|
|
1083
1097
|
return True
|
|
1084
1098
|
loop = asyncio.get_running_loop()
|
|
1085
1099
|
deadline_monotonic = None if timeout is None else (loop.time() + float(timeout))
|
|
1086
|
-
return await drainer(self.key.processing, deadline_monotonic=deadline_monotonic)
|
|
1100
|
+
return await _await_preserving_cancellation(drainer(self.key.processing, deadline_monotonic=deadline_monotonic))
|
|
1087
1101
|
|
|
1088
1102
|
def _build_lease_heartbeat(
|
|
1089
1103
|
self,
|
{redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/redis_message_queue.py
RENAMED
|
@@ -19,7 +19,7 @@ from redis_message_queue._config import (
|
|
|
19
19
|
validate_pending_backpressure_parameters,
|
|
20
20
|
)
|
|
21
21
|
from redis_message_queue._event import EventOperation, EventOutcome, QueueEvent
|
|
22
|
-
from redis_message_queue._exceptions import ConfigurationError, GatewayContractError
|
|
22
|
+
from redis_message_queue._exceptions import CleanupFailedError, ConfigurationError, GatewayContractError
|
|
23
23
|
from redis_message_queue._queue_key_manager import QueueKeyManager
|
|
24
24
|
from redis_message_queue._redis_cluster import validate_queue_keys_for_redis_cluster
|
|
25
25
|
from redis_message_queue._redis_gateway import RedisGateway
|
|
@@ -518,6 +518,17 @@ class RedisMessageQueue:
|
|
|
518
518
|
"'get_deduplication_key' is an async callable; "
|
|
519
519
|
"use the async RedisMessageQueue from redis_message_queue.asyncio instead"
|
|
520
520
|
)
|
|
521
|
+
if (
|
|
522
|
+
max_pending_length is not None
|
|
523
|
+
and pending_overload_policy == "drop_oldest"
|
|
524
|
+
and (deduplication or get_deduplication_key_was_configured)
|
|
525
|
+
):
|
|
526
|
+
raise ConfigurationError(
|
|
527
|
+
"'pending_overload_policy=drop_oldest' cannot be used with deduplication because dropped messages "
|
|
528
|
+
"leave their deduplication keys in Redis, causing future publishes of the same payload to be "
|
|
529
|
+
"silently suppressed. Use 'raise' or 'block' for deduplicated queues, or disable deduplication if "
|
|
530
|
+
"'drop_oldest' is required."
|
|
531
|
+
)
|
|
521
532
|
if not deduplication and get_deduplication_key_was_configured:
|
|
522
533
|
raise ConfigurationError("'get_deduplication_key' cannot be provided when 'deduplication' is disabled.")
|
|
523
534
|
if on_heartbeat_failure is not None and not callable(on_heartbeat_failure):
|
|
@@ -682,11 +693,13 @@ class RedisMessageQueue:
|
|
|
682
693
|
)
|
|
683
694
|
except Exception as exc:
|
|
684
695
|
logger.exception("on_event callback raised an exception")
|
|
685
|
-
warnings.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
696
|
+
with warnings.catch_warnings():
|
|
697
|
+
warnings.simplefilter("always", RuntimeWarning)
|
|
698
|
+
warnings.warn(
|
|
699
|
+
f"on_event callback raised {type(exc).__name__}",
|
|
700
|
+
RuntimeWarning,
|
|
701
|
+
stacklevel=2,
|
|
702
|
+
)
|
|
690
703
|
|
|
691
704
|
def publish(self, message: str | dict) -> bool:
|
|
692
705
|
"""Publish a message.
|
|
@@ -888,6 +901,7 @@ class RedisMessageQueue:
|
|
|
888
901
|
)
|
|
889
902
|
warnings.warn(_STALE_LEASE_NACK_WARNING, RuntimeWarning, stacklevel=2)
|
|
890
903
|
except BaseException as cleanup_exc:
|
|
904
|
+
# The handler exception is the user-visible failure; cleanup failure is secondary.
|
|
891
905
|
logger.exception("Failed to clean up message from processing queue")
|
|
892
906
|
self._emit_event(
|
|
893
907
|
"cleanup_failed",
|
|
@@ -913,7 +927,7 @@ class RedisMessageQueue:
|
|
|
913
927
|
applied = self._move_processed_message(self.key.completed, stored_message, lease_token)
|
|
914
928
|
else:
|
|
915
929
|
applied = self._remove_processed_message(stored_message, lease_token)
|
|
916
|
-
except
|
|
930
|
+
except Exception as cleanup_exc:
|
|
917
931
|
self._emit_event(
|
|
918
932
|
"cleanup_failed",
|
|
919
933
|
"failure",
|
|
@@ -922,7 +936,7 @@ class RedisMessageQueue:
|
|
|
922
936
|
exception_type=type(cleanup_exc).__name__,
|
|
923
937
|
duration_ms=_duration_ms(cleanup_started_at),
|
|
924
938
|
)
|
|
925
|
-
raise
|
|
939
|
+
raise CleanupFailedError("Cleanup after successful processing failed") from cleanup_exc
|
|
926
940
|
if self._enable_completed_queue:
|
|
927
941
|
self._emit_event(
|
|
928
942
|
"completed",
|
|
@@ -1047,6 +1061,13 @@ class RedisMessageQueue:
|
|
|
1047
1061
|
deadline_monotonic = None if timeout is None else (time.monotonic() + float(timeout))
|
|
1048
1062
|
return drainer(self.key.processing, deadline_monotonic=deadline_monotonic)
|
|
1049
1063
|
|
|
1064
|
+
def close(self, timeout: float | None = None) -> bool:
|
|
1065
|
+
"""Alias of :meth:`drain` for consistency with redis-py naming.
|
|
1066
|
+
|
|
1067
|
+
See :meth:`drain` for full semantics.
|
|
1068
|
+
"""
|
|
1069
|
+
return self.drain(timeout)
|
|
1070
|
+
|
|
1050
1071
|
def _build_lease_heartbeat(
|
|
1051
1072
|
self,
|
|
1052
1073
|
stored_message: MessageData,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_callable_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_queue_key_manager.py
RENAMED
|
File without changes
|
{redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_redis_cluster.py
RENAMED
|
File without changes
|
{redis_message_queue-6.0.0 → redis_message_queue-6.0.1}/redis_message_queue/_stored_message.py
RENAMED
|
File without changes
|
{redis_message_queue-6.0.0 → redis_message_queue-6.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
|