rabbitkit 0.19.1__tar.gz → 0.20.0__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 (124) hide show
  1. {rabbitkit-0.19.1/src/rabbitkit.egg-info → rabbitkit-0.20.0}/PKG-INFO +48 -18
  2. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/README.md +47 -17
  3. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/pyproject.toml +5 -2
  4. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/__init__.py +50 -1
  5. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/_version.py +1 -1
  6. rabbitkit-0.20.0/src/rabbitkit/async_/batch.py +431 -0
  7. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/async_/broker.py +646 -98
  8. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/async_/connection.py +277 -26
  9. rabbitkit-0.20.0/src/rabbitkit/async_/consumers.py +300 -0
  10. rabbitkit-0.20.0/src/rabbitkit/async_/pool.py +1110 -0
  11. rabbitkit-0.20.0/src/rabbitkit/async_/reconnect.py +340 -0
  12. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/async_/transport.py +841 -220
  13. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/dlq.py +101 -11
  14. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/concurrency.py +90 -30
  15. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/app.py +46 -16
  16. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/bulk.py +39 -7
  17. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/bulk_runner.py +63 -32
  18. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/config.py +465 -55
  19. rabbitkit-0.20.0/src/rabbitkit/core/dead_letter.py +242 -0
  20. rabbitkit-0.20.0/src/rabbitkit/core/errors.py +518 -0
  21. rabbitkit-0.20.0/src/rabbitkit/core/failure_copy.py +229 -0
  22. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/logging.py +69 -10
  23. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/message.py +108 -31
  24. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/pipeline.py +501 -67
  25. rabbitkit-0.20.0/src/rabbitkit/core/profiles.py +720 -0
  26. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/protocols.py +6 -0
  27. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/registry.py +54 -6
  28. rabbitkit-0.20.0/src/rabbitkit/core/reserved_headers.py +95 -0
  29. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/retry_handoff.py +18 -1
  30. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/route.py +78 -1
  31. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/router.py +8 -3
  32. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/settlement.py +66 -16
  33. rabbitkit-0.20.0/src/rabbitkit/core/signals.py +49 -0
  34. rabbitkit-0.20.0/src/rabbitkit/core/streams.py +580 -0
  35. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/topology.py +192 -1
  36. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/topology_dispatch.py +31 -3
  37. rabbitkit-0.20.0/src/rabbitkit/core/topology_validation.py +401 -0
  38. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/types.py +237 -4
  39. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/dlq.py +396 -20
  40. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/experimental/__init__.py +27 -5
  41. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/fastapi.py +10 -3
  42. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/health.py +31 -9
  43. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/highload/async_acker.py +37 -14
  44. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/highload/batch.py +44 -12
  45. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/management.py +5 -0
  46. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/__init__.py +11 -1
  47. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/deduplication.py +161 -48
  48. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/exception.py +24 -5
  49. rabbitkit-0.20.0/src/rabbitkit/middleware/metrics.py +1061 -0
  50. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/otel.py +62 -4
  51. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/retry.py +469 -187
  52. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/signing.py +9 -3
  53. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/timeout.py +35 -18
  54. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/queue_metrics.py +26 -4
  55. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/rpc.py +26 -3
  56. rabbitkit-0.20.0/src/rabbitkit/streams.py +46 -0
  57. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/sync/broker.py +348 -48
  58. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/sync/connection.py +29 -20
  59. rabbitkit-0.20.0/src/rabbitkit/sync/consumers.py +193 -0
  60. rabbitkit-0.20.0/src/rabbitkit/sync/deadlines.py +223 -0
  61. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/sync/pool.py +111 -0
  62. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/sync/transport.py +639 -148
  63. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/testing/__init__.py +2 -2
  64. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/testing/broker.py +31 -8
  65. {rabbitkit-0.19.1 → rabbitkit-0.20.0/src/rabbitkit.egg-info}/PKG-INFO +48 -18
  66. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit.egg-info/SOURCES.txt +10 -0
  67. rabbitkit-0.19.1/src/rabbitkit/async_/batch.py +0 -242
  68. rabbitkit-0.19.1/src/rabbitkit/async_/pool.py +0 -543
  69. rabbitkit-0.19.1/src/rabbitkit/core/errors.py +0 -266
  70. rabbitkit-0.19.1/src/rabbitkit/core/profiles.py +0 -483
  71. rabbitkit-0.19.1/src/rabbitkit/middleware/metrics.py +0 -501
  72. rabbitkit-0.19.1/src/rabbitkit/streams.py +0 -139
  73. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/LICENSE +0 -0
  74. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/setup.cfg +0 -0
  75. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/aio/__init__.py +0 -0
  76. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/async_/__init__.py +0 -0
  77. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/asyncapi/__init__.py +0 -0
  78. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/asyncapi/generator.py +0 -0
  79. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/asyncapi/schema.py +0 -0
  80. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/__init__.py +0 -0
  81. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/_utils.py +0 -0
  82. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/__init__.py +0 -0
  83. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/health.py +0 -0
  84. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/migrate.py +0 -0
  85. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/routes.py +0 -0
  86. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/run.py +0 -0
  87. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/shell.py +0 -0
  88. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/cli/commands/topology.py +0 -0
  89. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/__init__.py +0 -0
  90. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/env_config.py +0 -0
  91. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/path.py +0 -0
  92. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/quorum.py +0 -0
  93. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/core/sanitizer.py +0 -0
  94. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/dashboard/__init__.py +0 -0
  95. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/dashboard/app.py +0 -0
  96. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/di/__init__.py +0 -0
  97. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/di/context.py +0 -0
  98. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/di/depends.py +0 -0
  99. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/di/resolver.py +0 -0
  100. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/highload/__init__.py +0 -0
  101. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/highload/backpressure.py +0 -0
  102. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/locking.py +0 -0
  103. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/base.py +0 -0
  104. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/circuit_breaker.py +0 -0
  105. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/compression.py +0 -0
  106. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/error_classifier.py +0 -0
  107. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/middleware/rate_limit.py +0 -0
  108. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/py.typed +0 -0
  109. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/results/__init__.py +0 -0
  110. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/results/backend.py +0 -0
  111. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/results/middleware.py +0 -0
  112. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/serialization/__init__.py +0 -0
  113. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/serialization/base.py +0 -0
  114. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/serialization/json.py +0 -0
  115. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/serialization/msgspec.py +0 -0
  116. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/serialization/pipeline.py +0 -0
  117. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/sync/__init__.py +0 -0
  118. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/sync/batch.py +0 -0
  119. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/testing/app.py +0 -0
  120. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit/testing/fixtures.py +0 -0
  121. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit.egg-info/dependency_links.txt +0 -0
  122. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit.egg-info/entry_points.txt +0 -0
  123. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit.egg-info/requires.txt +0 -0
  124. {rabbitkit-0.19.1 → rabbitkit-0.20.0}/src/rabbitkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rabbitkit
3
- Version: 0.19.1
3
+ Version: 0.20.0
4
4
  Summary: Production-grade RabbitMQ toolkit — sync/async, decorator routing, retry, compression, full configurability
5
5
  Author-email: Talaat Magdy <talaatmagdy75@gmail.com>
6
6
  License-Expression: MIT
@@ -258,9 +258,12 @@ async def publish_order() -> None:
258
258
  outcome.raise_for_status()
259
259
  ```
260
260
 
261
- A publish can be `CONFIRMED`, `SENT`, `RETURNED`, `NACKED`, `TIMEOUT`, or
262
- `ERROR`. Application code can treat those as different states instead of
263
- assuming "publish called" means "message safe".
261
+ A publish can be `CONFIRMED`, `SENT`, `RETURNED`, `NACKED`, `NOT_SENT`
262
+ (definitely never left the client: safe to publish again), `UNKNOWN` or
263
+ `TIMEOUT` (may have reached the broker: publish again only with the same
264
+ `message_id`), or `ERROR`. `outcome.reason` says why. Application code can
265
+ treat those as different states instead of assuming "publish called" means
266
+ "message safe".
264
267
 
265
268
  ### 3. Add retry and DLQ handling
266
269
 
@@ -291,7 +294,11 @@ orders.created
291
294
  Transient failures retry with backoff. Permanent failures skip the ladder
292
295
  and go straight to the DLQ. By default, rejected messages do not disappear
293
296
  silently — every rejecting route gets a DLQ unless you explicitly opt into
294
- discard behavior.
297
+ discard behavior. A terminal failure reaches the DLQ as a broker-confirmed
298
+ copy carrying the final attempt's error class, sanitized message, failure
299
+ category, attempt count, timestamps and original queue/exchange/routing key;
300
+ `RetryConfig.max_retry_age` bounds the total retry time and
301
+ `RetryConfig.exponential(...)` builds the delay ladder.
295
302
 
296
303
  ### 4. Test it without RabbitMQ
297
304
 
@@ -420,8 +427,11 @@ def commit_batch(successful: list[RabbitMessage], failed: list[RabbitMessage]) -
420
427
 
421
428
  Opt-in **reliability profiles** (`critical_config()`, `broker.preflight("critical",
422
429
  management_client=...)`) verify confirms/mandatory/persistence, a 256 KiB body
423
- cap, quorum queues and a quorum retry chain — and report anything they cannot
424
- verify as *unverified*, never green. Retry/DLQ triage headers are sanitized by
430
+ cap, bounded in-flight publish bytes, quorum queues with at-least-once
431
+ dead-lettering, a quorum retry chain with a metadata-stamped DLQ copy, a
432
+ required processing deadline and no `ACK_FIRST` — and report anything they
433
+ cannot verify as *unverified*, never green
434
+ ([reliable messaging contract](docs/reliable-messaging.md)). Retry/DLQ triage headers are sanitized by
425
435
  default, and a failing delay-queue handoff backs off instead of hot-looping.
426
436
  `CoalescingAcker` (and `AsyncCoalescingAcker`) is the only place a cumulative
427
437
  `basic.ack(multiple=True)` may originate: it knows every delivery on the
@@ -475,9 +485,13 @@ duplicates). The rule is simple:
475
485
  | Handler raises forever | Retry ladder, then DLQ |
476
486
  | Malformed payload | Classified permanent, preserved in DLQ |
477
487
  | Reject with no DLX | Safe default auto-provisions a DLQ — no silent discard |
478
- | Retry publish times out | Original is **not** acked as if the retry succeeded |
488
+ | Retry publish times out | Original is **not** acked as if the retry succeeded; the delivery is held for a capped backoff before it is requeued, so it cannot hot-loop |
489
+ | Handler raises `AckMessage`/`RejectMessage` | Settled exactly as asked — never retried |
490
+ | Result publish fails after dedup | The redelivery is processed again, not acked as a duplicate |
491
+ | Infrastructure-managed topology | `TopologyMode.PASSIVE_ONLY` performs no writes; `broker.verify_topology(management_client)` detects type/argument/binding drift |
479
492
  | DLQ replay publish fails | DLQ message is **not** removed as if replay succeeded |
480
493
  | Message unroutable | Mandatory publishing returns a distinct `RETURNED` outcome |
494
+ | Oversized delivery (`ConsumerConfig.max_body_bytes`) | Dead-lettered as `invalid_message` before any middleware or the handler runs |
481
495
  | Broker blips | Readiness changes; liveness does **not** kill the pod |
482
496
  | Pod gets SIGTERM | Consumers stop first, in-flight work drains |
483
497
  | CI has no RabbitMQ | `TestBroker` runs the real pipeline in memory |
@@ -489,8 +503,8 @@ Settlement is a decision, not a side effect hidden in a callback.
489
503
 
490
504
  | Policy | Behavior | Use case |
491
505
  |---|---|---|
492
- | `AUTO` | Ack on success, retry/reject on failure | Most consumers |
493
- | `MANUAL` | Handler owns ack/nack/reject | Custom settlement flows |
506
+ | `AUTO` | Ack on success, retry/reject on failure (without retry: a transient failure is requeued once, then dead-lettered) | Most consumers |
507
+ | `MANUAL` | Handler owns ack/nack/reject; if it raises without settling, the `AUTO` failure settlement applies | Custom settlement flows |
494
508
  | `NACK_ON_ERROR` | Ack on success, nack on failure | Never silently accept failed work |
495
509
  | `ACK_FIRST` | Ack before the handler runs | At-most-once workloads |
496
510
 
@@ -511,14 +525,22 @@ handlers. Migrating existing classic queues to quorum? There's a tool:
511
525
  ## Observability
512
526
 
513
527
  Structured logs carry message context (`message_id`, `correlation_id`,
514
- routing, queue, handler, retry count, settlement, duration, error type) with
515
- secret redaction on by default. Metrics cover consumed/acked/nacked/
528
+ routing, queue, handler, retry count, settlement, duration, error type) and
529
+ redact secrets once logging is configured
530
+ (`RabbitConfig(logging=LoggingConfig())`; redaction is opt-in). Metrics cover consumed/acked/nacked/
516
531
  retried/dead-lettered counts, publish outcomes, handler latency,
517
532
  redeliveries, reconnects, confirm latency, in-flight handlers, broker/consumer
518
533
  lifecycle gauges, bulk-publish and settlement outcomes, retry-handoff
519
- failures, and — via the management API poller — queue depth and consumer lag. Tracing is standard OpenTelemetry
534
+ failures, resource alarms by cause (disk or memory, from `connection.blocked`), and — via the management API
535
+ poller — queue depth, consumer lag and quorum-queue member health. Tracing is standard OpenTelemetry
520
536
  (`pip install rabbitkit[otel]`): W3C context propagation over AMQP headers,
521
- one continuous trace from publish to consume.
537
+ one continuous trace from publish to consume. Prometheus alert rules with
538
+ `promtool` tests ship in `deploy/prometheus/rabbitkit-alerts.yaml`, each linked
539
+ to a runbook in `docs/runbooks/` and each threshold explained; they need only
540
+ rabbitkit's own metrics (the broker's `rabbitmq_prometheus` plugin is an
541
+ optional alternative group). For the full reliability contract (publish
542
+ outcomes, settlement, failure taxonomy, durability scope) see
543
+ `docs/reliable-messaging.md`.
522
544
 
523
545
  ## Advanced & experimental
524
546
 
@@ -528,12 +550,16 @@ with per-item outcomes, safe ack coalescing (`CoalescingAcker`), reliability
528
550
  profiles + `preflight`, pipelined sync confirms (`SyncBatchPublisher`), DLQ inspector + replay CLI, management API client,
529
551
  topology validation/drift/migration CLI, health watcher, circuit-breaker
530
552
  middleware (bring any `CircuitBreakerProtocol` implementation, e.g.
531
- pybreaker).
553
+ pybreaker), and RabbitMQ streams: `subscriber(stream_offset="first")` on a
554
+ `QueueType.STREAM` queue, per-delivery `message.stream_offset`, retention
555
+ options, and an `OffsetStore` protocol (in-memory by default; plug in your
556
+ own storage) so restarts and reconnects resume where they left off (at-least-once; no retry/DLQ on streams). See
557
+ [Streams](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/streams.md).
532
558
 
533
559
  **Experimental** (may change without a deprecation cycle — read the
534
560
  [stability policy](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/stability-policy.md)): RPC over direct reply-to,
535
- distributed locking, message signing, result backends, stream queues, the
536
- monitoring dashboard. Notable caveats: the default signing nonce cache is
561
+ distributed locking, message signing, result backends, the monitoring
562
+ dashboard. Notable caveats: the default signing nonce cache is
537
563
  per-process (use a shared cache for real replay protection), and never
538
564
  expose the dashboard publicly without authentication.
539
565
 
@@ -594,7 +620,9 @@ rabbitkit health myapp.main:broker
594
620
  ```
595
621
 
596
622
  The DLQ replay acks a message only after its republish is broker-confirmed —
597
- the recovery tool cannot itself lose messages.
623
+ the recovery tool cannot itself lose messages. Every replay is audit-logged
624
+ and stamps `x-rabbitkit-replay-count`; a message replayed 3 times is refused
625
+ unless you pass `--ignore-replay-limit`.
598
626
 
599
627
  <p align="center"><img src="https://raw.githubusercontent.com/talaatmagdyx/rabbitkit/main/assets/demo.svg" alt="rabbitkit dlq inspect and replay demo" width="720"></p>
600
628
 
@@ -735,6 +763,8 @@ Python ≥ 3.11 (tested: 3.11 / 3.12 / 3.13 / 3.14; 3.15 pre-release experimenta
735
763
  - [Full Guide](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/guide/full-guide.md)
736
764
  - [Message Safety](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/message-safety.md)
737
765
  - [Retry & DLQ](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/retry-and-dlq.md)
766
+ - [Reliable Messaging — contract & CRITICAL profile](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/reliable-messaging.md)
767
+ - [Streams — offsets, replay, retention](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/streams.md)
738
768
  - [Production Patterns — reference code](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/production/patterns.md)
739
769
  - [Scale & Reliability Handbook — millions of messages](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/production/scale.md)
740
770
  - [Production Checklist](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/production/checklist.md)
@@ -177,9 +177,12 @@ async def publish_order() -> None:
177
177
  outcome.raise_for_status()
178
178
  ```
179
179
 
180
- A publish can be `CONFIRMED`, `SENT`, `RETURNED`, `NACKED`, `TIMEOUT`, or
181
- `ERROR`. Application code can treat those as different states instead of
182
- assuming "publish called" means "message safe".
180
+ A publish can be `CONFIRMED`, `SENT`, `RETURNED`, `NACKED`, `NOT_SENT`
181
+ (definitely never left the client: safe to publish again), `UNKNOWN` or
182
+ `TIMEOUT` (may have reached the broker: publish again only with the same
183
+ `message_id`), or `ERROR`. `outcome.reason` says why. Application code can
184
+ treat those as different states instead of assuming "publish called" means
185
+ "message safe".
183
186
 
184
187
  ### 3. Add retry and DLQ handling
185
188
 
@@ -210,7 +213,11 @@ orders.created
210
213
  Transient failures retry with backoff. Permanent failures skip the ladder
211
214
  and go straight to the DLQ. By default, rejected messages do not disappear
212
215
  silently — every rejecting route gets a DLQ unless you explicitly opt into
213
- discard behavior.
216
+ discard behavior. A terminal failure reaches the DLQ as a broker-confirmed
217
+ copy carrying the final attempt's error class, sanitized message, failure
218
+ category, attempt count, timestamps and original queue/exchange/routing key;
219
+ `RetryConfig.max_retry_age` bounds the total retry time and
220
+ `RetryConfig.exponential(...)` builds the delay ladder.
214
221
 
215
222
  ### 4. Test it without RabbitMQ
216
223
 
@@ -339,8 +346,11 @@ def commit_batch(successful: list[RabbitMessage], failed: list[RabbitMessage]) -
339
346
 
340
347
  Opt-in **reliability profiles** (`critical_config()`, `broker.preflight("critical",
341
348
  management_client=...)`) verify confirms/mandatory/persistence, a 256 KiB body
342
- cap, quorum queues and a quorum retry chain — and report anything they cannot
343
- verify as *unverified*, never green. Retry/DLQ triage headers are sanitized by
349
+ cap, bounded in-flight publish bytes, quorum queues with at-least-once
350
+ dead-lettering, a quorum retry chain with a metadata-stamped DLQ copy, a
351
+ required processing deadline and no `ACK_FIRST` — and report anything they
352
+ cannot verify as *unverified*, never green
353
+ ([reliable messaging contract](docs/reliable-messaging.md)). Retry/DLQ triage headers are sanitized by
344
354
  default, and a failing delay-queue handoff backs off instead of hot-looping.
345
355
  `CoalescingAcker` (and `AsyncCoalescingAcker`) is the only place a cumulative
346
356
  `basic.ack(multiple=True)` may originate: it knows every delivery on the
@@ -394,9 +404,13 @@ duplicates). The rule is simple:
394
404
  | Handler raises forever | Retry ladder, then DLQ |
395
405
  | Malformed payload | Classified permanent, preserved in DLQ |
396
406
  | Reject with no DLX | Safe default auto-provisions a DLQ — no silent discard |
397
- | Retry publish times out | Original is **not** acked as if the retry succeeded |
407
+ | Retry publish times out | Original is **not** acked as if the retry succeeded; the delivery is held for a capped backoff before it is requeued, so it cannot hot-loop |
408
+ | Handler raises `AckMessage`/`RejectMessage` | Settled exactly as asked — never retried |
409
+ | Result publish fails after dedup | The redelivery is processed again, not acked as a duplicate |
410
+ | Infrastructure-managed topology | `TopologyMode.PASSIVE_ONLY` performs no writes; `broker.verify_topology(management_client)` detects type/argument/binding drift |
398
411
  | DLQ replay publish fails | DLQ message is **not** removed as if replay succeeded |
399
412
  | Message unroutable | Mandatory publishing returns a distinct `RETURNED` outcome |
413
+ | Oversized delivery (`ConsumerConfig.max_body_bytes`) | Dead-lettered as `invalid_message` before any middleware or the handler runs |
400
414
  | Broker blips | Readiness changes; liveness does **not** kill the pod |
401
415
  | Pod gets SIGTERM | Consumers stop first, in-flight work drains |
402
416
  | CI has no RabbitMQ | `TestBroker` runs the real pipeline in memory |
@@ -408,8 +422,8 @@ Settlement is a decision, not a side effect hidden in a callback.
408
422
 
409
423
  | Policy | Behavior | Use case |
410
424
  |---|---|---|
411
- | `AUTO` | Ack on success, retry/reject on failure | Most consumers |
412
- | `MANUAL` | Handler owns ack/nack/reject | Custom settlement flows |
425
+ | `AUTO` | Ack on success, retry/reject on failure (without retry: a transient failure is requeued once, then dead-lettered) | Most consumers |
426
+ | `MANUAL` | Handler owns ack/nack/reject; if it raises without settling, the `AUTO` failure settlement applies | Custom settlement flows |
413
427
  | `NACK_ON_ERROR` | Ack on success, nack on failure | Never silently accept failed work |
414
428
  | `ACK_FIRST` | Ack before the handler runs | At-most-once workloads |
415
429
 
@@ -430,14 +444,22 @@ handlers. Migrating existing classic queues to quorum? There's a tool:
430
444
  ## Observability
431
445
 
432
446
  Structured logs carry message context (`message_id`, `correlation_id`,
433
- routing, queue, handler, retry count, settlement, duration, error type) with
434
- secret redaction on by default. Metrics cover consumed/acked/nacked/
447
+ routing, queue, handler, retry count, settlement, duration, error type) and
448
+ redact secrets once logging is configured
449
+ (`RabbitConfig(logging=LoggingConfig())`; redaction is opt-in). Metrics cover consumed/acked/nacked/
435
450
  retried/dead-lettered counts, publish outcomes, handler latency,
436
451
  redeliveries, reconnects, confirm latency, in-flight handlers, broker/consumer
437
452
  lifecycle gauges, bulk-publish and settlement outcomes, retry-handoff
438
- failures, and — via the management API poller — queue depth and consumer lag. Tracing is standard OpenTelemetry
453
+ failures, resource alarms by cause (disk or memory, from `connection.blocked`), and — via the management API
454
+ poller — queue depth, consumer lag and quorum-queue member health. Tracing is standard OpenTelemetry
439
455
  (`pip install rabbitkit[otel]`): W3C context propagation over AMQP headers,
440
- one continuous trace from publish to consume.
456
+ one continuous trace from publish to consume. Prometheus alert rules with
457
+ `promtool` tests ship in `deploy/prometheus/rabbitkit-alerts.yaml`, each linked
458
+ to a runbook in `docs/runbooks/` and each threshold explained; they need only
459
+ rabbitkit's own metrics (the broker's `rabbitmq_prometheus` plugin is an
460
+ optional alternative group). For the full reliability contract (publish
461
+ outcomes, settlement, failure taxonomy, durability scope) see
462
+ `docs/reliable-messaging.md`.
441
463
 
442
464
  ## Advanced & experimental
443
465
 
@@ -447,12 +469,16 @@ with per-item outcomes, safe ack coalescing (`CoalescingAcker`), reliability
447
469
  profiles + `preflight`, pipelined sync confirms (`SyncBatchPublisher`), DLQ inspector + replay CLI, management API client,
448
470
  topology validation/drift/migration CLI, health watcher, circuit-breaker
449
471
  middleware (bring any `CircuitBreakerProtocol` implementation, e.g.
450
- pybreaker).
472
+ pybreaker), and RabbitMQ streams: `subscriber(stream_offset="first")` on a
473
+ `QueueType.STREAM` queue, per-delivery `message.stream_offset`, retention
474
+ options, and an `OffsetStore` protocol (in-memory by default; plug in your
475
+ own storage) so restarts and reconnects resume where they left off (at-least-once; no retry/DLQ on streams). See
476
+ [Streams](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/streams.md).
451
477
 
452
478
  **Experimental** (may change without a deprecation cycle — read the
453
479
  [stability policy](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/stability-policy.md)): RPC over direct reply-to,
454
- distributed locking, message signing, result backends, stream queues, the
455
- monitoring dashboard. Notable caveats: the default signing nonce cache is
480
+ distributed locking, message signing, result backends, the monitoring
481
+ dashboard. Notable caveats: the default signing nonce cache is
456
482
  per-process (use a shared cache for real replay protection), and never
457
483
  expose the dashboard publicly without authentication.
458
484
 
@@ -513,7 +539,9 @@ rabbitkit health myapp.main:broker
513
539
  ```
514
540
 
515
541
  The DLQ replay acks a message only after its republish is broker-confirmed —
516
- the recovery tool cannot itself lose messages.
542
+ the recovery tool cannot itself lose messages. Every replay is audit-logged
543
+ and stamps `x-rabbitkit-replay-count`; a message replayed 3 times is refused
544
+ unless you pass `--ignore-replay-limit`.
517
545
 
518
546
  <p align="center"><img src="https://raw.githubusercontent.com/talaatmagdyx/rabbitkit/main/assets/demo.svg" alt="rabbitkit dlq inspect and replay demo" width="720"></p>
519
547
 
@@ -654,6 +682,8 @@ Python ≥ 3.11 (tested: 3.11 / 3.12 / 3.13 / 3.14; 3.15 pre-release experimenta
654
682
  - [Full Guide](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/guide/full-guide.md)
655
683
  - [Message Safety](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/message-safety.md)
656
684
  - [Retry & DLQ](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/retry-and-dlq.md)
685
+ - [Reliable Messaging — contract & CRITICAL profile](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/reliable-messaging.md)
686
+ - [Streams — offsets, replay, retention](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/streams.md)
657
687
  - [Production Patterns — reference code](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/production/patterns.md)
658
688
  - [Scale & Reliability Handbook — millions of messages](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/production/scale.md)
659
689
  - [Production Checklist](https://github.com/talaatmagdyx/rabbitkit/blob/main/docs/production/checklist.md)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rabbitkit"
3
- version = "0.19.1"
3
+ version = "0.20.0"
4
4
  description = "Production-grade RabbitMQ toolkit — sync/async, decorator routing, retry, compression, full configurability"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -153,7 +153,10 @@ warn_unused_configs = true
153
153
  [tool.pytest.ini_options]
154
154
  asyncio_mode = "auto"
155
155
  testpaths = ["tests"]
156
- markers = ["integration: integration tests (may need RabbitMQ)"]
156
+ markers = [
157
+ "integration: integration tests (may need RabbitMQ)",
158
+ "cluster_chaos: slow multi-container failure tests (3-node cluster, toxiproxy); nightly only, not part of -m integration",
159
+ ]
157
160
  filterwarnings = [
158
161
  # rabbitkit's own operator-guidance warnings, exercised deliberately by
159
162
  # tests (dedicated pytest.warns tests still assert they fire — pytest.warns
@@ -38,16 +38,27 @@ from rabbitkit.core.config import (
38
38
  WorkerConfig,
39
39
  )
40
40
  from rabbitkit.core.errors import (
41
+ AuthenticationFailedError,
41
42
  BackpressureError,
42
43
  BrokerNotStartedError,
43
44
  ConfigurationError,
44
45
  ConfigValidationError,
46
+ DLQOperationDeniedError,
47
+ HandlerTimeoutError,
48
+ InboundMessageTooLargeError,
49
+ InfrastructureError,
50
+ InvalidMessageError,
45
51
  MessageTooLargeError,
46
52
  MissingDependencyError,
53
+ PermanentConnectionError,
54
+ ReconnectBudgetExhaustedError,
47
55
  SettlementError,
56
+ TerminalConnectionError,
57
+ TLSVerificationError,
48
58
  TopologyValidationError,
49
59
  UnsafeToBrowseError,
50
60
  UnsafeTopologyError,
61
+ VhostAccessRefusedError,
51
62
  )
52
63
  from rabbitkit.core.logging import DEFAULT_REDACT_KEYS, LoggingConfig, configure_structlog
53
64
  from rabbitkit.core.message import AckMessage, NackMessage, RabbitMessage, RejectMessage
@@ -79,30 +90,36 @@ from rabbitkit.core.settlement import (
79
90
  UnknownDeliveryError,
80
91
  )
81
92
  from rabbitkit.core.topology import RabbitExchange, RabbitQueue
93
+ from rabbitkit.core.topology_validation import TopologyFinding, TopologyReport, verify_topology
82
94
  from rabbitkit.core.types import (
83
95
  AckPolicy,
84
96
  BulkPublishStatus,
85
97
  ClassifiedError,
98
+ DeadLetterStrategy,
86
99
  DeduplicationMarkPolicy,
87
100
  DeliveryState,
101
+ ErrorCategory,
88
102
  ErrorSeverity,
89
103
  ExchangeType,
104
+ FailureCategory,
90
105
  FlushReason,
91
106
  HandoffState,
92
107
  MessageEnvelope,
93
108
  PreflightStatus,
94
109
  PublishOutcome,
110
+ PublishReason,
95
111
  PublishStatus,
96
112
  QueueType,
97
113
  RejectWithoutDLXPolicy,
98
114
  ReliabilityProfile,
99
115
  SettlementAction,
100
116
  SettlementItemStatus,
117
+ TerminalAction,
101
118
  TopologyMode,
102
119
  )
103
120
  from rabbitkit.di import Context, ContextRepo, Depends, DIResolver, Header, Path
104
121
  from rabbitkit.di.resolver import DependencyScope
105
- from rabbitkit.dlq import DLQInspector, ReplayResult
122
+ from rabbitkit.dlq import DLQInspector, ReplayRequest, ReplayResult
106
123
  from rabbitkit.fastapi import rabbitkit_lifespan
107
124
  from rabbitkit.health import (
108
125
  AsyncHealthWatcher,
@@ -153,6 +170,13 @@ from rabbitkit.serialization.pipeline import (
153
170
  RawDecoder,
154
171
  SerializationPipeline,
155
172
  )
173
+ from rabbitkit.streams import (
174
+ InMemoryOffsetStore,
175
+ OffsetStore,
176
+ StreamConsumerConfig,
177
+ StreamOffset,
178
+ StreamOffsetType,
179
+ )
156
180
  from rabbitkit.sync.batch import SyncBatchPublisher
157
181
  from rabbitkit.sync.broker import SyncBroker
158
182
 
@@ -168,6 +192,7 @@ __all__ = [
168
192
  "AsyncCoalescingAcker",
169
193
  "AsyncHealthWatcher",
170
194
  "AsyncWorkerPool",
195
+ "AuthenticationFailedError",
171
196
  "BackpressureConfig",
172
197
  "BackpressureError",
173
198
  "BatchAckConfig",
@@ -201,26 +226,35 @@ __all__ = [
201
226
  "CoordinatorError",
202
227
  "DIResolver",
203
228
  "DLQInspector",
229
+ "DLQOperationDeniedError",
204
230
  "DataclassDecoder",
231
+ "DeadLetterStrategy",
205
232
  "DeduplicationConfig",
206
233
  "DeduplicationMarkPolicy",
207
234
  "DeduplicationMiddleware",
208
235
  "DeliveryState",
209
236
  "DependencyScope",
210
237
  "Depends",
238
+ "ErrorCategory",
211
239
  "ErrorSanitizer",
212
240
  "ErrorSeverity",
213
241
  "ExchangeType",
242
+ "FailureCategory",
214
243
  "FlowController",
215
244
  "FlushItem",
216
245
  "FlushReason",
217
246
  "FlushReport",
218
247
  "GroupFlushReport",
248
+ "HandlerTimeoutError",
219
249
  "HandoffState",
220
250
  "Header",
221
251
  "HealthCheckConfig",
222
252
  "HealthStatus",
223
253
  "HealthWatcher",
254
+ "InMemoryOffsetStore",
255
+ "InboundMessageTooLargeError",
256
+ "InfrastructureError",
257
+ "InvalidMessageError",
224
258
  "JsonParser",
225
259
  "LedgerFullError",
226
260
  "LoggingConfig",
@@ -235,7 +269,9 @@ __all__ = [
235
269
  "MissingDependencyError",
236
270
  "NackMessage",
237
271
  "OTelTracingMiddleware",
272
+ "OffsetStore",
238
273
  "Path",
274
+ "PermanentConnectionError",
239
275
  "PolicyTemplate",
240
276
  "PoolConfig",
241
277
  "PreflightCheck",
@@ -245,6 +281,7 @@ __all__ = [
245
281
  "PrometheusCollector",
246
282
  "PublishOutcome",
247
283
  "PublishPreparer",
284
+ "PublishReason",
248
285
  "PublishStatus",
249
286
  "PublisherConfig",
250
287
  "PydanticDecoder",
@@ -260,9 +297,11 @@ __all__ = [
260
297
  "RateLimitConfig",
261
298
  "RateLimitMiddleware",
262
299
  "RawDecoder",
300
+ "ReconnectBudgetExhaustedError",
263
301
  "RejectMessage",
264
302
  "RejectWithoutDLXPolicy",
265
303
  "ReliabilityProfile",
304
+ "ReplayRequest",
266
305
  "ReplayResult",
267
306
  "RetryConfig",
268
307
  "RetryDisabled",
@@ -283,14 +322,23 @@ __all__ = [
283
322
  "SettlementReportError",
284
323
  "SocketConfig",
285
324
  "StaleGenerationError",
325
+ "StreamConsumerConfig",
326
+ "StreamOffset",
327
+ "StreamOffsetType",
286
328
  "SyncBatchPublisher",
287
329
  "SyncBroker",
288
330
  "SyncWorkerPool",
331
+ "TLSVerificationError",
332
+ "TerminalAction",
333
+ "TerminalConnectionError",
334
+ "TopologyFinding",
289
335
  "TopologyMode",
336
+ "TopologyReport",
290
337
  "TopologyValidationError",
291
338
  "UnknownDeliveryError",
292
339
  "UnsafeToBrowseError",
293
340
  "UnsafeTopologyError",
341
+ "VhostAccessRefusedError",
294
342
  "WorkerConfig",
295
343
  "__version__",
296
344
  "apply_profile",
@@ -312,4 +360,5 @@ __all__ = [
312
360
  "standard_config",
313
361
  "start_metrics_server",
314
362
  "validate_profile",
363
+ "verify_topology",
315
364
  ]
@@ -1,3 +1,3 @@
1
1
  """rabbitkit version — single source of truth."""
2
2
 
3
- __version__ = "0.19.1"
3
+ __version__ = "0.20.0"