rabbitkit 0.19.0__tar.gz → 0.19.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.
- {rabbitkit-0.19.0/src/rabbitkit.egg-info → rabbitkit-0.19.1}/PKG-INFO +1 -1
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/pyproject.toml +1 -1
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/_version.py +1 -1
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/async_/pool.py +76 -5
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/async_/transport.py +24 -89
- {rabbitkit-0.19.0 → rabbitkit-0.19.1/src/rabbitkit.egg-info}/PKG-INFO +1 -1
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/LICENSE +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/README.md +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/setup.cfg +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/aio/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/async_/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/async_/batch.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/async_/broker.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/async_/connection.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/asyncapi/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/asyncapi/generator.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/asyncapi/schema.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/_utils.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/dlq.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/health.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/migrate.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/routes.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/run.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/shell.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/cli/commands/topology.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/concurrency.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/app.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/bulk.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/bulk_runner.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/config.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/env_config.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/errors.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/logging.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/message.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/path.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/pipeline.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/profiles.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/protocols.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/quorum.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/registry.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/retry_handoff.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/route.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/router.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/sanitizer.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/settlement.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/topology.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/topology_dispatch.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/core/types.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/dashboard/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/dashboard/app.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/di/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/di/context.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/di/depends.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/di/resolver.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/dlq.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/experimental/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/fastapi.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/health.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/highload/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/highload/async_acker.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/highload/backpressure.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/highload/batch.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/locking.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/management.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/base.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/circuit_breaker.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/compression.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/deduplication.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/error_classifier.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/exception.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/metrics.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/otel.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/rate_limit.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/retry.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/signing.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/middleware/timeout.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/py.typed +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/queue_metrics.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/results/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/results/backend.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/results/middleware.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/rpc.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/serialization/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/serialization/base.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/serialization/json.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/serialization/msgspec.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/serialization/pipeline.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/streams.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/sync/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/sync/batch.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/sync/broker.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/sync/connection.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/sync/pool.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/sync/transport.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/testing/__init__.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/testing/app.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/testing/broker.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit/testing/fixtures.py +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit.egg-info/SOURCES.txt +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit.egg-info/dependency_links.txt +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit.egg-info/entry_points.txt +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/src/rabbitkit.egg-info/requires.txt +0 -0
- {rabbitkit-0.19.0 → rabbitkit-0.19.1}/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.
|
|
3
|
+
Version: 0.19.1
|
|
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
|
|
@@ -13,6 +13,7 @@ import asyncio
|
|
|
13
13
|
import contextlib
|
|
14
14
|
import logging
|
|
15
15
|
import random
|
|
16
|
+
import time
|
|
16
17
|
from collections.abc import Callable
|
|
17
18
|
from contextlib import asynccontextmanager
|
|
18
19
|
from typing import Any
|
|
@@ -22,6 +23,9 @@ from rabbitkit.core.config import ConnectionConfig, PoolConfig, SecurityConfig
|
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
26
|
+
#: Seconds between "Channel pool exhausted" warnings from one pool.
|
|
27
|
+
_EXHAUSTED_WARNING_INTERVAL = 60.0
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
class AsyncChannelPool:
|
|
27
31
|
"""Async channel pool.
|
|
@@ -42,11 +46,14 @@ class AsyncChannelPool:
|
|
|
42
46
|
publisher_confirms: bool = True,
|
|
43
47
|
on_channel_opened: Callable[[], None] | None = None,
|
|
44
48
|
on_channel_rebuilt: Callable[[], None] | None = None,
|
|
49
|
+
on_return_raises: bool = False,
|
|
45
50
|
) -> None:
|
|
46
51
|
self._connection = connection
|
|
47
52
|
self._pool_size = pool_size
|
|
48
53
|
self._acquire_timeout = acquire_timeout
|
|
49
54
|
self._publisher_confirms = publisher_confirms
|
|
55
|
+
self._on_return_raises = on_return_raises
|
|
56
|
+
self._exhausted_warned_at = -_EXHAUSTED_WARNING_INTERVAL
|
|
50
57
|
self._pool: asyncio.Queue[Any] = asyncio.Queue(maxsize=pool_size)
|
|
51
58
|
self._lock = asyncio.Lock()
|
|
52
59
|
self._created = 0
|
|
@@ -111,7 +118,7 @@ class AsyncChannelPool:
|
|
|
111
118
|
# turns the hang into a raise so ``acquire_publisher_channel``
|
|
112
119
|
# can rebuild the connection.
|
|
113
120
|
async with asyncio.timeout(self._acquire_timeout):
|
|
114
|
-
channel = await self._connection.channel(
|
|
121
|
+
channel = await self._connection.channel(**self._channel_kwargs())
|
|
115
122
|
except BaseException:
|
|
116
123
|
# creation failed (or timed out) — give the reserved slot back.
|
|
117
124
|
async with self._lock:
|
|
@@ -128,7 +135,15 @@ class AsyncChannelPool:
|
|
|
128
135
|
# Pool exhausted — wait with timeout to avoid deadlocks. R-timeout:
|
|
129
136
|
# ``asyncio.timeout`` (3.11+) replaces ``asyncio.wait_for`` to avoid
|
|
130
137
|
# the wrapper-task overhead.
|
|
131
|
-
|
|
138
|
+
# Once a minute at WARNING, then DEBUG: under a burst every waiting
|
|
139
|
+
# publish lands here (2000 concurrent mandatory publishes logged 11874
|
|
140
|
+
# warnings), and a wait that runs out still raises TimeoutError below.
|
|
141
|
+
now = time.monotonic()
|
|
142
|
+
log = logger.debug
|
|
143
|
+
if now - self._exhausted_warned_at >= _EXHAUSTED_WARNING_INTERVAL:
|
|
144
|
+
self._exhausted_warned_at = now
|
|
145
|
+
log = logger.warning
|
|
146
|
+
log(
|
|
132
147
|
"Channel pool exhausted (pool_size=%d, created=%d). "
|
|
133
148
|
"Waiting up to %.1fs for a channel to be released. "
|
|
134
149
|
"Consider increasing PoolConfig.channel_pool_size.",
|
|
@@ -152,6 +167,12 @@ class AsyncChannelPool:
|
|
|
152
167
|
self._in_use.add(channel)
|
|
153
168
|
return channel
|
|
154
169
|
|
|
170
|
+
def _channel_kwargs(self) -> dict[str, bool]:
|
|
171
|
+
kwargs = {"publisher_confirms": self._publisher_confirms}
|
|
172
|
+
if self._on_return_raises:
|
|
173
|
+
kwargs["on_return_raises"] = True
|
|
174
|
+
return kwargs
|
|
175
|
+
|
|
155
176
|
async def release(self, channel: Any) -> None:
|
|
156
177
|
"""Release a channel back to the pool."""
|
|
157
178
|
async with self._lock:
|
|
@@ -265,6 +286,9 @@ class AsyncConnectionPool:
|
|
|
265
286
|
self._publisher_connection: Any | None = None
|
|
266
287
|
self._consumer_connection: Any | None = None
|
|
267
288
|
self._publisher_channel_pool: AsyncChannelPool | None = None
|
|
289
|
+
# mandatory=True publishes: confirmed channels that raise on Basic.Return,
|
|
290
|
+
# one publish per channel at a time — see acquire_mandatory_channel()
|
|
291
|
+
self._mandatory_channel_pool: AsyncChannelPool | None = None
|
|
268
292
|
self._lock = asyncio.Lock()
|
|
269
293
|
self._prewarmed = False
|
|
270
294
|
|
|
@@ -378,10 +402,12 @@ class AsyncConnectionPool:
|
|
|
378
402
|
if self._publisher_connection is not stale:
|
|
379
403
|
return # another worker already rebuilt it
|
|
380
404
|
old_pool, self._publisher_channel_pool = self._publisher_channel_pool, None
|
|
405
|
+
old_mandatory, self._mandatory_channel_pool = self._mandatory_channel_pool, None
|
|
381
406
|
old_conn, self._publisher_connection = self._publisher_connection, None
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
407
|
+
for stale_pool in (old_pool, old_mandatory):
|
|
408
|
+
if stale_pool is not None:
|
|
409
|
+
with contextlib.suppress(Exception):
|
|
410
|
+
await asyncio.wait_for(stale_pool.close_all(), timeout=timeout)
|
|
385
411
|
if old_conn is not None and not old_conn.is_closed:
|
|
386
412
|
with contextlib.suppress(Exception):
|
|
387
413
|
await asyncio.wait_for(old_conn.close(), timeout=timeout)
|
|
@@ -398,12 +424,57 @@ class AsyncConnectionPool:
|
|
|
398
424
|
if self._publisher_channel_pool is not None:
|
|
399
425
|
await self._publisher_channel_pool.release(channel)
|
|
400
426
|
|
|
427
|
+
def _mandatory_pool(self) -> AsyncChannelPool:
|
|
428
|
+
if self._mandatory_channel_pool is None:
|
|
429
|
+
assert self._publisher_connection is not None
|
|
430
|
+
self._mandatory_channel_pool = AsyncChannelPool(
|
|
431
|
+
self._publisher_connection,
|
|
432
|
+
pool_size=self._pool_config.channel_pool_size,
|
|
433
|
+
acquire_timeout=self._pool_config.channel_acquire_timeout,
|
|
434
|
+
publisher_confirms=True,
|
|
435
|
+
on_return_raises=True,
|
|
436
|
+
on_channel_opened=self._on_channel_opened,
|
|
437
|
+
on_channel_rebuilt=self._on_channel_rebuilt,
|
|
438
|
+
)
|
|
439
|
+
return self._mandatory_channel_pool
|
|
440
|
+
|
|
441
|
+
async def acquire_mandatory_channel(self) -> Any:
|
|
442
|
+
"""A channel for one ``mandatory=True`` publish: confirms on, and
|
|
443
|
+
``on_return_raises=True`` so an unroutable return raises.
|
|
444
|
+
|
|
445
|
+
One publish per channel at a time, like the confirmed pool. A publish
|
|
446
|
+
the broker refuses (403, 404, 406: an unknown exchange, a forged
|
|
447
|
+
``user_id``) closes its channel, and every operation still in flight
|
|
448
|
+
on that channel fails with it. One shared mandatory channel therefore
|
|
449
|
+
let one refused publish fail every concurrent innocent one. Closed
|
|
450
|
+
channels are discarded on release. Self-heals a wedged connection the
|
|
451
|
+
same way :meth:`acquire_publisher_channel` does.
|
|
452
|
+
"""
|
|
453
|
+
if self._publisher_connection is None:
|
|
454
|
+
await self.get_publisher_connection()
|
|
455
|
+
try:
|
|
456
|
+
return await self._mandatory_pool().acquire()
|
|
457
|
+
except asyncio.CancelledError:
|
|
458
|
+
raise
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
logger.warning("Mandatory channel acquire failed (%s); rebuilding connection", exc)
|
|
461
|
+
await self._rebuild_publisher_connection(stale=self._publisher_connection)
|
|
462
|
+
return await self._mandatory_pool().acquire()
|
|
463
|
+
|
|
464
|
+
async def release_mandatory_channel(self, channel: Any) -> None:
|
|
465
|
+
"""Return a mandatory-publish channel to its pool (a closed one is discarded)."""
|
|
466
|
+
if self._mandatory_channel_pool is not None:
|
|
467
|
+
await self._mandatory_channel_pool.release(channel)
|
|
468
|
+
|
|
401
469
|
async def close_all(self) -> None:
|
|
402
470
|
"""Close all channel pools and connections."""
|
|
403
471
|
async with self._lock:
|
|
404
472
|
if self._publisher_channel_pool is not None:
|
|
405
473
|
await self._publisher_channel_pool.close_all()
|
|
406
474
|
self._publisher_channel_pool = None
|
|
475
|
+
if self._mandatory_channel_pool is not None:
|
|
476
|
+
await self._mandatory_channel_pool.close_all()
|
|
477
|
+
self._mandatory_channel_pool = None
|
|
407
478
|
|
|
408
479
|
for conn in [self._publisher_connection, self._consumer_connection]:
|
|
409
480
|
if conn is not None:
|
|
@@ -176,21 +176,14 @@ class AsyncTransportImpl:
|
|
|
176
176
|
self._fast_publish_channel: Any | None = None
|
|
177
177
|
self._fast_channel_lock: asyncio.Lock = asyncio.Lock()
|
|
178
178
|
|
|
179
|
-
# H1:
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
# (confirms follow confirm_delivery, which may be False) guarantee that.
|
|
184
|
-
self._mandatory_publish_channel: Any | None = None
|
|
185
|
-
# M3: in-flight mandatory publishes on the shared channel + a
|
|
186
|
-
# deferred-recycle flag set by a confirm timeout (closed at zero).
|
|
187
|
-
self._mandatory_in_flight = 0
|
|
188
|
-
self._mandatory_channel_recycle = False
|
|
179
|
+
# H1: mandatory=True publishes use the connection pool's mandatory
|
|
180
|
+
# channels (acquire_mandatory_channel): always confirmed and
|
|
181
|
+
# on_return_raises=True, independent of confirm_delivery, one publish
|
|
182
|
+
# per channel so a refused publish fails only itself.
|
|
189
183
|
# m3: bindings recorded for re-apply after robust reconnect (they are
|
|
190
184
|
# not in RobustChannel's restoration registry — see bind_queue).
|
|
191
185
|
self._recorded_bindings: list[tuple[str, str, str, str, dict[str, Any] | None]] = []
|
|
192
186
|
self._binding_restore_tasks: set[Any] = set()
|
|
193
|
-
self._mandatory_channel_lock: asyncio.Lock = asyncio.Lock()
|
|
194
187
|
|
|
195
188
|
# Backpressure callbacks (FlowController registers here). Each is a
|
|
196
189
|
# zero-arg callable; aio-pika's blocked/unblocked frames are adapted.
|
|
@@ -510,15 +503,7 @@ class AsyncTransportImpl:
|
|
|
510
503
|
pass
|
|
511
504
|
self._fast_publish_channel = None
|
|
512
505
|
|
|
513
|
-
#
|
|
514
|
-
if self._mandatory_publish_channel is not None and not self._mandatory_publish_channel.is_closed:
|
|
515
|
-
try:
|
|
516
|
-
await self._mandatory_publish_channel.close()
|
|
517
|
-
except Exception: # pragma: no cover — best effort close, network errors only
|
|
518
|
-
pass
|
|
519
|
-
self._mandatory_publish_channel = None
|
|
520
|
-
|
|
521
|
-
await self._conn_pool.close_all()
|
|
506
|
+
await self._conn_pool.close_all() # includes the mandatory-publish channels
|
|
522
507
|
except Exception as e:
|
|
523
508
|
logger.warning("Error during disconnect: %s", e)
|
|
524
509
|
finally:
|
|
@@ -632,36 +617,6 @@ class AsyncTransportImpl:
|
|
|
632
617
|
self._fire_channel_rebuilt()
|
|
633
618
|
return self._fast_publish_channel
|
|
634
619
|
|
|
635
|
-
async def _get_mandatory_channel(self) -> Any:
|
|
636
|
-
"""Return the dedicated always-confirmed channel for mandatory=True
|
|
637
|
-
publishes, (re)opening if needed.
|
|
638
|
-
|
|
639
|
-
H1: ``on_return_raises=True`` makes an unroutable ``Basic.Return``
|
|
640
|
-
raise ``aio_pika.exceptions.PublishError`` (caught by
|
|
641
|
-
:meth:`_publish_on_channel` and mapped to ``PublishStatus.RETURNED``)
|
|
642
|
-
instead of silently resolving the confirmation with the returned
|
|
643
|
-
message — indistinguishable from success otherwise. This channel is
|
|
644
|
-
used for every ``mandatory=True`` publish regardless of the broker's
|
|
645
|
-
``confirm_delivery`` setting, since reliable return detection needs
|
|
646
|
-
confirms + on_return_raises unconditionally.
|
|
647
|
-
"""
|
|
648
|
-
ch = self._mandatory_publish_channel
|
|
649
|
-
if ch is not None and not ch.is_closed:
|
|
650
|
-
return ch
|
|
651
|
-
async with self._mandatory_channel_lock:
|
|
652
|
-
ch = self._mandatory_publish_channel
|
|
653
|
-
if ch is not None and not ch.is_closed: # pragma: no cover — concurrent path
|
|
654
|
-
return ch
|
|
655
|
-
conn = self._conn_pool._publisher_connection
|
|
656
|
-
if conn is None:
|
|
657
|
-
raise RuntimeError("Publisher connection is not available")
|
|
658
|
-
is_rebuild = ch is not None # replacing a closed/recycled channel
|
|
659
|
-
self._mandatory_publish_channel = await conn.channel(publisher_confirms=True, on_return_raises=True)
|
|
660
|
-
self._fire_channel_opened()
|
|
661
|
-
if is_rebuild:
|
|
662
|
-
self._fire_channel_rebuilt()
|
|
663
|
-
return self._mandatory_publish_channel
|
|
664
|
-
|
|
665
620
|
def _build_aio_message(self, envelope: MessageEnvelope) -> Any:
|
|
666
621
|
"""Build an aio_pika.Message from a MessageEnvelope.
|
|
667
622
|
|
|
@@ -697,8 +652,8 @@ class AsyncTransportImpl:
|
|
|
697
652
|
|
|
698
653
|
H1: a ``mandatory=True`` publish that the broker cannot route raises
|
|
699
654
|
``aio_pika.exceptions.PublishError`` when the channel has
|
|
700
|
-
``on_return_raises=True`` (only true for
|
|
701
|
-
|
|
655
|
+
``on_return_raises=True`` (only true for the connection pool's
|
|
656
|
+
mandatory channels — regular pool/fast channels default to
|
|
702
657
|
``on_return_raises=False`` and would otherwise resolve the confirmation
|
|
703
658
|
with the returned message instead of raising, indistinguishable from
|
|
704
659
|
success). Mapped to ``PublishStatus.RETURNED`` so callers keying off
|
|
@@ -817,10 +772,13 @@ class AsyncTransportImpl:
|
|
|
817
772
|
"PRECONDITION_FAILED - fast reply consumer does not exist".
|
|
818
773
|
|
|
819
774
|
H1: a ``mandatory=True`` envelope (that isn't a direct reply-to
|
|
820
|
-
request) always publishes
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
pool channel
|
|
775
|
+
request) always publishes on a mandatory channel of its own
|
|
776
|
+
(``AsyncConnectionPool.acquire_mandatory_channel``): confirmed and
|
|
777
|
+
``on_return_raises=True`` regardless of ``confirm_delivery``, since
|
|
778
|
+
neither the fast nor the regular pool channel reliably reports an
|
|
779
|
+
unroutable ``Basic.Return``. One publish per channel: a publish the
|
|
780
|
+
broker refuses closes its channel and fails everything in flight on
|
|
781
|
+
it, so a shared channel let one refused publish fail its siblings.
|
|
824
782
|
|
|
825
783
|
The confirmed (pooled-channel) path is handled by
|
|
826
784
|
:meth:`_publish_confirmed`, OUTSIDE this method's own try/except
|
|
@@ -835,36 +793,7 @@ class AsyncTransportImpl:
|
|
|
835
793
|
if not channel.is_closed:
|
|
836
794
|
return await self._publish_on_channel(channel, envelope)
|
|
837
795
|
|
|
838
|
-
if envelope.mandatory:
|
|
839
|
-
channel = await self._get_mandatory_channel()
|
|
840
|
-
# M3 (architect review): this single persistent channel is
|
|
841
|
-
# shared by ALL concurrent mandatory publishes. Closing it the
|
|
842
|
-
# instant OUR publish times out cascades channel-closed errors
|
|
843
|
-
# into every sibling still awaiting its own confirm (spurious
|
|
844
|
-
# NACKED/ERROR → caller retries → duplicates). Ref-count
|
|
845
|
-
# in-flight publishes and recycle the channel only when the
|
|
846
|
-
# last one resolves; _get_mandatory_channel() lazily reopens.
|
|
847
|
-
self._mandatory_in_flight += 1
|
|
848
|
-
try:
|
|
849
|
-
outcome = await self._publish_on_channel(channel, envelope)
|
|
850
|
-
finally:
|
|
851
|
-
self._mandatory_in_flight -= 1
|
|
852
|
-
if outcome.status == PublishStatus.TIMEOUT:
|
|
853
|
-
self._mandatory_channel_recycle = True
|
|
854
|
-
current = self._mandatory_publish_channel
|
|
855
|
-
if (
|
|
856
|
-
self._mandatory_channel_recycle
|
|
857
|
-
and self._mandatory_in_flight == 0
|
|
858
|
-
and current is not None
|
|
859
|
-
and channel is current
|
|
860
|
-
and not current.is_closed
|
|
861
|
-
):
|
|
862
|
-
self._mandatory_channel_recycle = False
|
|
863
|
-
with contextlib.suppress(Exception):
|
|
864
|
-
await current.close()
|
|
865
|
-
return outcome
|
|
866
|
-
|
|
867
|
-
if not self._confirm_delivery:
|
|
796
|
+
if not envelope.mandatory and not self._confirm_delivery:
|
|
868
797
|
# Fast path: persistent channel, no confirm wait, no pool overhead
|
|
869
798
|
message = self._build_aio_message(envelope)
|
|
870
799
|
channel = await self._get_fast_channel()
|
|
@@ -917,17 +846,23 @@ class AsyncTransportImpl:
|
|
|
917
846
|
which discards it since it's closed). No wait/recovery, or a
|
|
918
847
|
second failure, returns ERROR — never raises.
|
|
919
848
|
"""
|
|
849
|
+
pool = self._conn_pool
|
|
850
|
+
acquire, release = (
|
|
851
|
+
(pool.acquire_mandatory_channel, pool.release_mandatory_channel)
|
|
852
|
+
if envelope.mandatory
|
|
853
|
+
else (pool.acquire_publisher_channel, pool.release_publisher_channel)
|
|
854
|
+
)
|
|
920
855
|
last_exc: Exception | None = None
|
|
921
856
|
for attempt in range(2):
|
|
922
857
|
channel = None
|
|
923
858
|
try:
|
|
924
|
-
channel = await
|
|
859
|
+
channel = await acquire()
|
|
925
860
|
outcome = await self._publish_on_channel(channel, envelope)
|
|
926
861
|
except Exception as e:
|
|
927
862
|
last_exc = e
|
|
928
863
|
if channel is not None:
|
|
929
864
|
with contextlib.suppress(Exception):
|
|
930
|
-
await
|
|
865
|
+
await release(channel)
|
|
931
866
|
if attempt == 0 and _is_connection_error(e) and await self._wait_for_recovery():
|
|
932
867
|
logger.warning(
|
|
933
868
|
"Publish hit a connection error, retrying once after recovery: exchange=%s routing_key=%s",
|
|
@@ -952,7 +887,7 @@ class AsyncTransportImpl:
|
|
|
952
887
|
with contextlib.suppress(Exception):
|
|
953
888
|
await channel.close()
|
|
954
889
|
with contextlib.suppress(Exception):
|
|
955
|
-
await
|
|
890
|
+
await release(channel)
|
|
956
891
|
return outcome
|
|
957
892
|
|
|
958
893
|
return PublishOutcome( # pragma: no cover — defensive; the loop always returns above
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rabbitkit
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.1
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|