redis-message-queue 8.2.8__tar.gz → 8.2.10__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.
Files changed (26) hide show
  1. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/PKG-INFO +23 -15
  2. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/README.md +22 -14
  3. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/pyproject.toml +2 -2
  4. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/asyncio/redis_message_queue.py +41 -30
  5. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/redis_message_queue.py +42 -28
  6. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/.gitignore +0 -0
  7. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/LICENSE +0 -0
  8. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/__init__.py +0 -0
  9. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_abstract_redis_gateway.py +0 -0
  10. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_callable_utils.py +0 -0
  11. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_config.py +0 -0
  12. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_event.py +0 -0
  13. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_exceptions.py +0 -0
  14. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_payload_limits.py +0 -0
  15. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_queue_key_manager.py +0 -0
  16. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_redis_cluster.py +0 -0
  17. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_redis_gateway.py +0 -0
  18. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/_stored_message.py +0 -0
  19. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/asyncio/__init__.py +0 -0
  20. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/asyncio/_abstract_redis_gateway.py +0 -0
  21. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/asyncio/_redis_gateway.py +0 -0
  22. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/interrupt_handler/__init__.py +0 -0
  23. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/interrupt_handler/_event_driven.py +0 -0
  24. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/interrupt_handler/_implementation.py +0 -0
  25. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/redis_message_queue/interrupt_handler/_interface.py +0 -0
  26. {redis_message_queue-8.2.8 → redis_message_queue-8.2.10}/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.8
3
+ Version: 8.2.10
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.8,<9.0.0"
37
+ pip install "redis-message-queue>=8.2.10,<9.0.0"
38
38
  ```
39
39
 
40
40
  Requires Redis server >= 6.2.
@@ -848,6 +848,11 @@ Pre-commit and mid-flight exceptions:
848
848
  exceptions. Under an ambiguous lost response, Redis may have committed
849
849
  despite the exception. Treat them as "operation did not succeed from the
850
850
  caller's perspective", not "Redis did not commit".
851
+ - Visibility-timeout claim-store write failures raise
852
+ `ClaimStoreFailedError` and emit `claim/failure`. When the compensating
853
+ return-to-pending write succeeds, the payload is back in pending; if
854
+ return-to-pending also fails, the payload remains in processing so there is
855
+ still a live queue copy.
851
856
 
852
857
  #### Drain events
853
858
 
@@ -875,11 +880,6 @@ The following operations have no `on_event` surface by design:
875
880
  preserves queue safety on Cluster `CROSSSLOT` rejection but cannot be
876
881
  observed through `on_event`. Operators watching key-TTL behavior or Redis
877
882
  slow logs can detect orphans.
878
- - **VT claim-store OOM compensation:** if the visibility-timeout Lua script
879
- cannot store the claim result, it removes the message from processing, pushes
880
- it back to pending, and returns `false`. Python translates that into
881
- `claim_empty/skipped`, the same shape as an empty poll. This is intentional
882
- fail-safe behavior; the message is not lost.
883
883
  - **`drop_oldest` evictions:** when publish backpressure uses
884
884
  `pending_overload_policy="drop_oldest"`, the oldest pending message is
885
885
  discarded before the new message is enqueued. The successful enqueue emits
@@ -890,14 +890,22 @@ The following operations have no `on_event` surface by design:
890
890
  terminal operation's failure event. There is no per-attempt event for those
891
891
  paths.
892
892
 
893
- The public exception hierarchy is rooted at `RedisMessageQueueError`.
894
- Configuration value/combinations raise `ConfigurationError` (also a
895
- `ValueError`), custom gateway contract violations raise `GatewayContractError`
896
- (also a `TypeError`), and Lua `redis.error_reply(...)` failures raise
897
- `LuaScriptError` (also a redis-py `ResponseError`). Publish overload raises
898
- `QueueBackpressureError`; publish after explicit drain raises
899
- `QueueDrainedError`. `CleanupFailedError` and `RetryBudgetExhaustedError` are
900
- reserved categories for cleanup and retry surfaces.
893
+ The public exception hierarchy is rooted at `RedisMessageQueueError`. The
894
+ current exported queue-owned exception classes are:
895
+
896
+ - `RedisMessageQueueError` (base)
897
+ - `ClaimStoreFailedError` - visibility-timeout claim metadata could not be stored
898
+ - `ConfigurationError` - invalid constructor args; also a `ValueError`
899
+ - `DrainFailedError` - drain pending-claim recovery failed
900
+ - `GatewayContractError` - custom gateway protocol violation; also a `TypeError`
901
+ - `LuaScriptError` - Lua `redis.error_reply(...)`; also a redis-py `ResponseError`
902
+ - `MalformedStoredMessageError` - stored value is not a valid RMQ envelope
903
+ - `PayloadTooLargeError` - serialized payload exceeds `max_payload_bytes`; also a `ValueError`
904
+ - `PayloadTooDeepError` - payload nesting exceeds `max_payload_depth`; also a `ValueError`
905
+ - `QueueBackpressureError` - `pending_overload_policy="raise"` rejected enqueue
906
+ - `QueueDrainedError` - `publish()` called after explicit drain/aclose
907
+ - `CleanupFailedError` - cleanup after handler completion failed
908
+ - `RetryBudgetExhaustedError` - Redis retry budget exhausted; also a redis-py `RedisError`
901
909
 
902
910
  ## Known limitations
903
911
 
@@ -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.8,<9.0.0"
14
+ pip install "redis-message-queue>=8.2.10,<9.0.0"
15
15
  ```
16
16
 
17
17
  Requires Redis server >= 6.2.
@@ -825,6 +825,11 @@ Pre-commit and mid-flight exceptions:
825
825
  exceptions. Under an ambiguous lost response, Redis may have committed
826
826
  despite the exception. Treat them as "operation did not succeed from the
827
827
  caller's perspective", not "Redis did not commit".
828
+ - Visibility-timeout claim-store write failures raise
829
+ `ClaimStoreFailedError` and emit `claim/failure`. When the compensating
830
+ return-to-pending write succeeds, the payload is back in pending; if
831
+ return-to-pending also fails, the payload remains in processing so there is
832
+ still a live queue copy.
828
833
 
829
834
  #### Drain events
830
835
 
@@ -852,11 +857,6 @@ The following operations have no `on_event` surface by design:
852
857
  preserves queue safety on Cluster `CROSSSLOT` rejection but cannot be
853
858
  observed through `on_event`. Operators watching key-TTL behavior or Redis
854
859
  slow logs can detect orphans.
855
- - **VT claim-store OOM compensation:** if the visibility-timeout Lua script
856
- cannot store the claim result, it removes the message from processing, pushes
857
- it back to pending, and returns `false`. Python translates that into
858
- `claim_empty/skipped`, the same shape as an empty poll. This is intentional
859
- fail-safe behavior; the message is not lost.
860
860
  - **`drop_oldest` evictions:** when publish backpressure uses
861
861
  `pending_overload_policy="drop_oldest"`, the oldest pending message is
862
862
  discarded before the new message is enqueued. The successful enqueue emits
@@ -867,14 +867,22 @@ The following operations have no `on_event` surface by design:
867
867
  terminal operation's failure event. There is no per-attempt event for those
868
868
  paths.
869
869
 
870
- The public exception hierarchy is rooted at `RedisMessageQueueError`.
871
- Configuration value/combinations raise `ConfigurationError` (also a
872
- `ValueError`), custom gateway contract violations raise `GatewayContractError`
873
- (also a `TypeError`), and Lua `redis.error_reply(...)` failures raise
874
- `LuaScriptError` (also a redis-py `ResponseError`). Publish overload raises
875
- `QueueBackpressureError`; publish after explicit drain raises
876
- `QueueDrainedError`. `CleanupFailedError` and `RetryBudgetExhaustedError` are
877
- reserved categories for cleanup and retry surfaces.
870
+ The public exception hierarchy is rooted at `RedisMessageQueueError`. The
871
+ current exported queue-owned exception classes are:
872
+
873
+ - `RedisMessageQueueError` (base)
874
+ - `ClaimStoreFailedError` - visibility-timeout claim metadata could not be stored
875
+ - `ConfigurationError` - invalid constructor args; also a `ValueError`
876
+ - `DrainFailedError` - drain pending-claim recovery failed
877
+ - `GatewayContractError` - custom gateway protocol violation; also a `TypeError`
878
+ - `LuaScriptError` - Lua `redis.error_reply(...)`; also a redis-py `ResponseError`
879
+ - `MalformedStoredMessageError` - stored value is not a valid RMQ envelope
880
+ - `PayloadTooLargeError` - serialized payload exceeds `max_payload_bytes`; also a `ValueError`
881
+ - `PayloadTooDeepError` - payload nesting exceeds `max_payload_depth`; also a `ValueError`
882
+ - `QueueBackpressureError` - `pending_overload_policy="raise"` rejected enqueue
883
+ - `QueueDrainedError` - `publish()` called after explicit drain/aclose
884
+ - `CleanupFailedError` - cleanup after handler completion failed
885
+ - `RetryBudgetExhaustedError` - Redis retry budget exhausted; also a redis-py `RedisError`
878
886
 
879
887
  ## Known limitations
880
888
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "redis-message-queue"
3
- version = "8.2.8"
3
+ version = "8.2.10"
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.8"
51
+ current_version = "8.2.10"
52
52
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
53
53
  serialize = ["{major}.{minor}.{patch}"]
54
54
  search = "{current_version}"
@@ -87,6 +87,12 @@ def _warning_exception_name(exc: BaseException) -> str:
87
87
  return type(exc).__name__
88
88
 
89
89
 
90
+ def _warn_runtime_warning(message: str, *, stacklevel: int) -> None:
91
+ with warnings.catch_warnings():
92
+ warnings.simplefilter("always", RuntimeWarning)
93
+ warnings.warn(message, RuntimeWarning, stacklevel=stacklevel + 1)
94
+
95
+
90
96
  def _find_non_string_dict_keys(value: object) -> list[object]:
91
97
  non_str_keys: list[object] = []
92
98
  seen: set[int] = set()
@@ -505,6 +511,29 @@ class _LeaseHeartbeat:
505
511
  stacklevel=1,
506
512
  )
507
513
 
514
+ async def _report_renewal_failure(self, exc: BaseException) -> None:
515
+ if self._stop_event.is_set():
516
+ return
517
+ logger.exception("Failed to renew message lease")
518
+ await self._emit(
519
+ "lease_renew_failed",
520
+ "failure",
521
+ message_id=self._message_id,
522
+ lease_token_hash=self._lease_token_hash,
523
+ exception_type=type(exc).__name__,
524
+ error=exc,
525
+ )
526
+ with warnings.catch_warnings():
527
+ warnings.simplefilter("always", RuntimeWarning)
528
+ warnings.warn(
529
+ "Failed to renew message lease "
530
+ f"({_warning_exception_name(exc)}); message will be reclaimed by another consumer "
531
+ "when the visibility timeout expires",
532
+ RuntimeWarning,
533
+ stacklevel=1,
534
+ )
535
+ await self._invoke_failure_callback()
536
+
508
537
  async def _run(self) -> None:
509
538
  try:
510
539
  while True:
@@ -525,30 +554,14 @@ class _LeaseHeartbeat:
525
554
  f"gateway.renew_message_lease() must return bool, got {type(renewed).__name__}. "
526
555
  "See AbstractRedisGateway.renew_message_lease for the full contract."
527
556
  )
528
- except asyncio.CancelledError:
529
- raise
557
+ except asyncio.CancelledError as exc:
558
+ current_task = asyncio.current_task()
559
+ if self._stop_event.is_set() or (current_task is not None and current_task.cancelling() > 0):
560
+ raise
561
+ await self._report_renewal_failure(exc)
562
+ return
530
563
  except Exception as exc:
531
- if self._stop_event.is_set():
532
- return
533
- logger.exception("Failed to renew message lease")
534
- await self._emit(
535
- "lease_renew_failed",
536
- "failure",
537
- message_id=self._message_id,
538
- lease_token_hash=self._lease_token_hash,
539
- exception_type=type(exc).__name__,
540
- error=exc,
541
- )
542
- with warnings.catch_warnings():
543
- warnings.simplefilter("always", RuntimeWarning)
544
- warnings.warn(
545
- "Failed to renew message lease "
546
- f"({_warning_exception_name(exc)}); message will be reclaimed by another consumer "
547
- "when the visibility timeout expires",
548
- RuntimeWarning,
549
- stacklevel=1,
550
- )
551
- await self._invoke_failure_callback()
564
+ await self._report_renewal_failure(exc)
552
565
  return
553
566
  if not renewed:
554
567
  await self._emit(
@@ -1173,7 +1186,7 @@ class RedisMessageQueue:
1173
1186
  "visibility timeouts are in use."
1174
1187
  )
1175
1188
  logger.warning(no_lease_token_warning)
1176
- warnings.warn(no_lease_token_warning, RuntimeWarning, stacklevel=2)
1189
+ _warn_runtime_warning(no_lease_token_warning, stacklevel=2)
1177
1190
 
1178
1191
  if lease_token is None and self._requires_claimed_message:
1179
1192
  raise GatewayContractError(
@@ -1273,7 +1286,7 @@ class RedisMessageQueue:
1273
1286
  message_id=message_id,
1274
1287
  lease_token_hash=lease_token_hash,
1275
1288
  )
1276
- warnings.warn(_STALE_LEASE_NACK_WARNING, RuntimeWarning, stacklevel=2)
1289
+ _warn_runtime_warning(_STALE_LEASE_NACK_WARNING, stacklevel=2)
1277
1290
  except BaseException as cleanup_exc:
1278
1291
  # The handler exception is the user-visible failure; cleanup failure is secondary.
1279
1292
  logger.exception("Failed to clean up message from processing queue")
@@ -1286,10 +1299,9 @@ class RedisMessageQueue:
1286
1299
  error=cleanup_exc,
1287
1300
  duration_ms=_duration_ms(cleanup_started_at),
1288
1301
  )
1289
- warnings.warn(
1302
+ _warn_runtime_warning(
1290
1303
  f"Cleanup raised after handler exception ({_warning_exception_name(cleanup_exc)}); "
1291
1304
  "see logs for both tracebacks",
1292
- RuntimeWarning,
1293
1305
  stacklevel=2,
1294
1306
  )
1295
1307
  raise
@@ -1349,7 +1361,7 @@ class RedisMessageQueue:
1349
1361
  message_id=message_id,
1350
1362
  lease_token_hash=lease_token_hash,
1351
1363
  )
1352
- warnings.warn(_STALE_LEASE_ACK_WARNING, RuntimeWarning, stacklevel=2)
1364
+ _warn_runtime_warning(_STALE_LEASE_ACK_WARNING, stacklevel=2)
1353
1365
  finished_without_error = True
1354
1366
  finally:
1355
1367
  if lease_heartbeat is not None:
@@ -1438,9 +1450,8 @@ class RedisMessageQueue:
1438
1450
  exception_type=type(exc).__name__,
1439
1451
  error=exc,
1440
1452
  )
1441
- warnings.warn(
1453
+ _warn_runtime_warning(
1442
1454
  f"Failed to trim queue {destination_queue} ({type(exc).__name__}); list may exceed max_*_length",
1443
- RuntimeWarning,
1444
1455
  stacklevel=3,
1445
1456
  )
1446
1457
 
@@ -100,6 +100,12 @@ def _warning_exception_name(exc: BaseException) -> str:
100
100
  return type(exc).__name__
101
101
 
102
102
 
103
+ def _warn_runtime_warning(message: str, *, stacklevel: int) -> None:
104
+ with warnings.catch_warnings():
105
+ warnings.simplefilter("always", RuntimeWarning)
106
+ warnings.warn(message, RuntimeWarning, stacklevel=stacklevel + 1)
107
+
108
+
103
109
  def _find_non_string_dict_keys(value: object) -> list[object]:
104
110
  non_str_keys: list[object] = []
105
111
  seen: set[int] = set()
@@ -339,12 +345,16 @@ def _close_or_cancel_awaitable(awaitable: object) -> None:
339
345
  try:
340
346
  close()
341
347
  return
348
+ except asyncio.CancelledError:
349
+ pass
342
350
  except Exception:
343
351
  pass
344
352
  cancel = getattr(awaitable, "cancel", None)
345
353
  if callable(cancel):
346
354
  try:
347
355
  cancel()
356
+ except asyncio.CancelledError:
357
+ pass
348
358
  except Exception:
349
359
  pass
350
360
 
@@ -472,6 +482,29 @@ class _LeaseHeartbeat:
472
482
  stacklevel=1,
473
483
  )
474
484
 
485
+ def _report_renewal_failure(self, exc: BaseException) -> None:
486
+ if self._stop_event.is_set():
487
+ return
488
+ logger.exception("Failed to renew message lease")
489
+ self._emit(
490
+ "lease_renew_failed",
491
+ "failure",
492
+ message_id=self._message_id,
493
+ lease_token_hash=self._lease_token_hash,
494
+ exception_type=type(exc).__name__,
495
+ error=exc,
496
+ )
497
+ with warnings.catch_warnings():
498
+ warnings.simplefilter("always", RuntimeWarning)
499
+ warnings.warn(
500
+ "Failed to renew message lease "
501
+ f"({_warning_exception_name(exc)}); message will be reclaimed by another consumer "
502
+ "when the visibility timeout expires",
503
+ RuntimeWarning,
504
+ stacklevel=1,
505
+ )
506
+ self._invoke_failure_callback()
507
+
475
508
  def _run(self) -> None:
476
509
  # No explicit _is_interrupted() check here. Heartbeat lifetime is owned
477
510
  # by process_message, which sets _stop_event in its finally block on any
@@ -488,28 +521,11 @@ class _LeaseHeartbeat:
488
521
  f"gateway.renew_message_lease() must return bool, got {type(renewed).__name__}. "
489
522
  "See AbstractRedisGateway.renew_message_lease for the full contract."
490
523
  )
524
+ except asyncio.CancelledError as exc:
525
+ self._report_renewal_failure(exc)
526
+ return
491
527
  except Exception as exc:
492
- if self._stop_event.is_set():
493
- return
494
- logger.exception("Failed to renew message lease")
495
- self._emit(
496
- "lease_renew_failed",
497
- "failure",
498
- message_id=self._message_id,
499
- lease_token_hash=self._lease_token_hash,
500
- exception_type=type(exc).__name__,
501
- error=exc,
502
- )
503
- with warnings.catch_warnings():
504
- warnings.simplefilter("always", RuntimeWarning)
505
- warnings.warn(
506
- "Failed to renew message lease "
507
- f"({_warning_exception_name(exc)}); message will be reclaimed by another consumer "
508
- "when the visibility timeout expires",
509
- RuntimeWarning,
510
- stacklevel=1,
511
- )
512
- self._invoke_failure_callback()
528
+ self._report_renewal_failure(exc)
513
529
  return
514
530
  if not renewed:
515
531
  self._emit(
@@ -1140,7 +1156,7 @@ class RedisMessageQueue:
1140
1156
  "visibility timeouts are in use."
1141
1157
  )
1142
1158
  logger.warning(no_lease_token_warning)
1143
- warnings.warn(no_lease_token_warning, RuntimeWarning, stacklevel=2)
1159
+ _warn_runtime_warning(no_lease_token_warning, stacklevel=2)
1144
1160
 
1145
1161
  if lease_token is None and self._requires_claimed_message:
1146
1162
  raise GatewayContractError(
@@ -1235,7 +1251,7 @@ class RedisMessageQueue:
1235
1251
  message_id=message_id,
1236
1252
  lease_token_hash=lease_token_hash,
1237
1253
  )
1238
- warnings.warn(_STALE_LEASE_NACK_WARNING, RuntimeWarning, stacklevel=2)
1254
+ _warn_runtime_warning(_STALE_LEASE_NACK_WARNING, stacklevel=2)
1239
1255
  except BaseException as cleanup_exc:
1240
1256
  # The handler exception is the user-visible failure; cleanup failure is secondary.
1241
1257
  logger.exception("Failed to clean up message from processing queue")
@@ -1248,10 +1264,9 @@ class RedisMessageQueue:
1248
1264
  error=cleanup_exc,
1249
1265
  duration_ms=_duration_ms(cleanup_started_at),
1250
1266
  )
1251
- warnings.warn(
1267
+ _warn_runtime_warning(
1252
1268
  f"Cleanup raised after handler exception ({_warning_exception_name(cleanup_exc)}); "
1253
1269
  "see logs for both tracebacks",
1254
- RuntimeWarning,
1255
1270
  stacklevel=2,
1256
1271
  )
1257
1272
  raise
@@ -1310,7 +1325,7 @@ class RedisMessageQueue:
1310
1325
  message_id=message_id,
1311
1326
  lease_token_hash=lease_token_hash,
1312
1327
  )
1313
- warnings.warn(_STALE_LEASE_ACK_WARNING, RuntimeWarning, stacklevel=2)
1328
+ _warn_runtime_warning(_STALE_LEASE_ACK_WARNING, stacklevel=2)
1314
1329
  finally:
1315
1330
  if lease_heartbeat is not None:
1316
1331
  lease_heartbeat.stop()
@@ -1400,9 +1415,8 @@ class RedisMessageQueue:
1400
1415
  exception_type=type(exc).__name__,
1401
1416
  error=exc,
1402
1417
  )
1403
- warnings.warn(
1418
+ _warn_runtime_warning(
1404
1419
  f"Failed to trim queue {destination_queue} ({type(exc).__name__}); list may exceed max_*_length",
1405
- RuntimeWarning,
1406
1420
  stacklevel=3,
1407
1421
  )
1408
1422