rabbitkit 0.9.0__py3-none-any.whl
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/__init__.py +201 -0
- rabbitkit/_version.py +3 -0
- rabbitkit/aio/__init__.py +31 -0
- rabbitkit/async_/__init__.py +9 -0
- rabbitkit/async_/batch.py +213 -0
- rabbitkit/async_/broker.py +1123 -0
- rabbitkit/async_/connection.py +274 -0
- rabbitkit/async_/pool.py +363 -0
- rabbitkit/async_/transport.py +877 -0
- rabbitkit/asyncapi/__init__.py +5 -0
- rabbitkit/asyncapi/generator.py +219 -0
- rabbitkit/asyncapi/schema.py +98 -0
- rabbitkit/cli/__init__.py +77 -0
- rabbitkit/cli/_utils.py +38 -0
- rabbitkit/cli/commands/__init__.py +0 -0
- rabbitkit/cli/commands/dlq.py +190 -0
- rabbitkit/cli/commands/health.py +34 -0
- rabbitkit/cli/commands/migrate.py +570 -0
- rabbitkit/cli/commands/routes.py +88 -0
- rabbitkit/cli/commands/run.py +144 -0
- rabbitkit/cli/commands/shell.py +72 -0
- rabbitkit/cli/commands/topology.py +346 -0
- rabbitkit/concurrency.py +451 -0
- rabbitkit/core/__init__.py +5 -0
- rabbitkit/core/app.py +323 -0
- rabbitkit/core/config.py +849 -0
- rabbitkit/core/env_config.py +251 -0
- rabbitkit/core/errors.py +199 -0
- rabbitkit/core/logging.py +261 -0
- rabbitkit/core/message.py +235 -0
- rabbitkit/core/path.py +53 -0
- rabbitkit/core/pipeline.py +1289 -0
- rabbitkit/core/protocols.py +349 -0
- rabbitkit/core/registry.py +284 -0
- rabbitkit/core/route.py +329 -0
- rabbitkit/core/router.py +142 -0
- rabbitkit/core/topology.py +261 -0
- rabbitkit/core/topology_dispatch.py +74 -0
- rabbitkit/core/types.py +324 -0
- rabbitkit/dashboard/__init__.py +5 -0
- rabbitkit/dashboard/app.py +212 -0
- rabbitkit/di/__init__.py +19 -0
- rabbitkit/di/context.py +193 -0
- rabbitkit/di/depends.py +42 -0
- rabbitkit/di/resolver.py +503 -0
- rabbitkit/dlq.py +320 -0
- rabbitkit/experimental/__init__.py +50 -0
- rabbitkit/fastapi.py +91 -0
- rabbitkit/health.py +654 -0
- rabbitkit/highload/__init__.py +10 -0
- rabbitkit/highload/backpressure.py +514 -0
- rabbitkit/highload/batch.py +448 -0
- rabbitkit/locking.py +277 -0
- rabbitkit/management.py +470 -0
- rabbitkit/middleware/__init__.py +27 -0
- rabbitkit/middleware/base.py +125 -0
- rabbitkit/middleware/circuit_breaker.py +131 -0
- rabbitkit/middleware/compression.py +267 -0
- rabbitkit/middleware/deduplication.py +651 -0
- rabbitkit/middleware/error_classifier.py +43 -0
- rabbitkit/middleware/exception.py +105 -0
- rabbitkit/middleware/metrics.py +440 -0
- rabbitkit/middleware/otel.py +203 -0
- rabbitkit/middleware/rate_limit.py +247 -0
- rabbitkit/middleware/retry.py +540 -0
- rabbitkit/middleware/signing.py +682 -0
- rabbitkit/middleware/timeout.py +291 -0
- rabbitkit/py.typed +0 -0
- rabbitkit/queue_metrics.py +174 -0
- rabbitkit/results/__init__.py +6 -0
- rabbitkit/results/backend.py +102 -0
- rabbitkit/results/middleware.py +123 -0
- rabbitkit/rpc.py +632 -0
- rabbitkit/serialization/__init__.py +25 -0
- rabbitkit/serialization/base.py +35 -0
- rabbitkit/serialization/json.py +122 -0
- rabbitkit/serialization/msgspec.py +136 -0
- rabbitkit/serialization/pipeline.py +255 -0
- rabbitkit/streams.py +139 -0
- rabbitkit/sync/__init__.py +11 -0
- rabbitkit/sync/batch.py +595 -0
- rabbitkit/sync/broker.py +996 -0
- rabbitkit/sync/connection.py +209 -0
- rabbitkit/sync/pool.py +262 -0
- rabbitkit/sync/transport.py +1085 -0
- rabbitkit/testing/__init__.py +20 -0
- rabbitkit/testing/app.py +99 -0
- rabbitkit/testing/broker.py +540 -0
- rabbitkit/testing/fixtures.py +56 -0
- rabbitkit-0.9.0.dist-info/METADATA +575 -0
- rabbitkit-0.9.0.dist-info/RECORD +95 -0
- rabbitkit-0.9.0.dist-info/WHEEL +5 -0
- rabbitkit-0.9.0.dist-info/entry_points.txt +2 -0
- rabbitkit-0.9.0.dist-info/licenses/LICENSE +21 -0
- rabbitkit-0.9.0.dist-info/top_level.txt +1 -0
rabbitkit/__init__.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""rabbitkit — Production-grade RabbitMQ toolkit."""
|
|
2
|
+
|
|
3
|
+
from rabbitkit import experimental
|
|
4
|
+
from rabbitkit._version import __version__
|
|
5
|
+
from rabbitkit.async_.batch import AsyncBatchPublisher
|
|
6
|
+
from rabbitkit.async_.broker import AsyncBroker
|
|
7
|
+
from rabbitkit.asyncapi import AsyncAPIGeneratorConfig, generate_asyncapi_doc, generate_asyncapi_json
|
|
8
|
+
from rabbitkit.concurrency import AsyncWorkerPool, SyncWorkerPool
|
|
9
|
+
from rabbitkit.core.app import AppState, RabbitApp
|
|
10
|
+
from rabbitkit.core.config import (
|
|
11
|
+
RETRY_DISABLED,
|
|
12
|
+
BackpressureConfig,
|
|
13
|
+
BatchAckConfig,
|
|
14
|
+
BatchPublishConfig,
|
|
15
|
+
CompressionConfig,
|
|
16
|
+
ConnectionConfig,
|
|
17
|
+
ConsumerConfig,
|
|
18
|
+
DeduplicationConfig,
|
|
19
|
+
HealthCheckConfig,
|
|
20
|
+
MetricsConfig,
|
|
21
|
+
PoolConfig,
|
|
22
|
+
PublisherConfig,
|
|
23
|
+
RabbitConfig,
|
|
24
|
+
RetryConfig,
|
|
25
|
+
RetryDisabled,
|
|
26
|
+
SafetyConfig,
|
|
27
|
+
SecurityConfig,
|
|
28
|
+
SocketConfig,
|
|
29
|
+
SSLConfig,
|
|
30
|
+
WorkerConfig,
|
|
31
|
+
)
|
|
32
|
+
from rabbitkit.core.errors import (
|
|
33
|
+
BackpressureError,
|
|
34
|
+
ConfigurationError,
|
|
35
|
+
MissingDependencyError,
|
|
36
|
+
UnsafeTopologyError,
|
|
37
|
+
)
|
|
38
|
+
from rabbitkit.core.logging import DEFAULT_REDACT_KEYS, LoggingConfig, configure_structlog
|
|
39
|
+
from rabbitkit.core.message import AckMessage, NackMessage, RabbitMessage, RejectMessage
|
|
40
|
+
from rabbitkit.core.router import RabbitRouter
|
|
41
|
+
from rabbitkit.core.topology import RabbitExchange, RabbitQueue
|
|
42
|
+
from rabbitkit.core.types import (
|
|
43
|
+
AckPolicy,
|
|
44
|
+
ClassifiedError,
|
|
45
|
+
DeduplicationMarkPolicy,
|
|
46
|
+
ErrorSeverity,
|
|
47
|
+
ExchangeType,
|
|
48
|
+
MessageEnvelope,
|
|
49
|
+
PublishOutcome,
|
|
50
|
+
PublishStatus,
|
|
51
|
+
QueueType,
|
|
52
|
+
RejectWithoutDLXPolicy,
|
|
53
|
+
TopologyMode,
|
|
54
|
+
)
|
|
55
|
+
from rabbitkit.di import Context, ContextRepo, Depends, DIResolver, Header, Path
|
|
56
|
+
from rabbitkit.di.resolver import DependencyScope
|
|
57
|
+
from rabbitkit.dlq import DLQInspector, ReplayResult
|
|
58
|
+
from rabbitkit.fastapi import rabbitkit_lifespan
|
|
59
|
+
from rabbitkit.health import (
|
|
60
|
+
AsyncHealthWatcher,
|
|
61
|
+
BrokerHealthResult,
|
|
62
|
+
HealthStatus,
|
|
63
|
+
HealthWatcher,
|
|
64
|
+
broker_health_check,
|
|
65
|
+
broker_health_check_async,
|
|
66
|
+
broker_liveness,
|
|
67
|
+
broker_liveness_async,
|
|
68
|
+
broker_readiness,
|
|
69
|
+
broker_readiness_async,
|
|
70
|
+
)
|
|
71
|
+
from rabbitkit.highload.backpressure import FlowController
|
|
72
|
+
from rabbitkit.highload.batch import BatchAcker, BatchPublisher
|
|
73
|
+
from rabbitkit.management import ManagementConfig, RabbitManagementClient
|
|
74
|
+
from rabbitkit.middleware.circuit_breaker import CircuitBreakerMiddleware, CircuitBreakerOpenError
|
|
75
|
+
from rabbitkit.middleware.deduplication import DeduplicationMiddleware
|
|
76
|
+
from rabbitkit.middleware.metrics import (
|
|
77
|
+
MetricsCollector,
|
|
78
|
+
MetricsMiddleware,
|
|
79
|
+
PrometheusCollector,
|
|
80
|
+
metrics_app,
|
|
81
|
+
start_metrics_server,
|
|
82
|
+
)
|
|
83
|
+
from rabbitkit.middleware.otel import OTelTracingMiddleware
|
|
84
|
+
from rabbitkit.middleware.rate_limit import RateLimitConfig, RateLimitMiddleware
|
|
85
|
+
from rabbitkit.queue_metrics import QueueMetricsPoller
|
|
86
|
+
from rabbitkit.serialization.pipeline import (
|
|
87
|
+
DataclassDecoder,
|
|
88
|
+
JsonParser,
|
|
89
|
+
MessageDecoder,
|
|
90
|
+
MessageParser,
|
|
91
|
+
PydanticDecoder,
|
|
92
|
+
RawDecoder,
|
|
93
|
+
SerializationPipeline,
|
|
94
|
+
)
|
|
95
|
+
from rabbitkit.sync.batch import SyncBatchPublisher
|
|
96
|
+
from rabbitkit.sync.broker import SyncBroker
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
"DEFAULT_REDACT_KEYS",
|
|
100
|
+
"RETRY_DISABLED",
|
|
101
|
+
"AckMessage",
|
|
102
|
+
"AckPolicy",
|
|
103
|
+
"AppState",
|
|
104
|
+
"AsyncAPIGeneratorConfig",
|
|
105
|
+
"AsyncBatchPublisher",
|
|
106
|
+
"AsyncBroker",
|
|
107
|
+
"AsyncHealthWatcher",
|
|
108
|
+
"AsyncWorkerPool",
|
|
109
|
+
"BackpressureConfig",
|
|
110
|
+
"BackpressureError",
|
|
111
|
+
"BatchAckConfig",
|
|
112
|
+
"BatchAcker",
|
|
113
|
+
"BatchPublishConfig",
|
|
114
|
+
"BatchPublisher",
|
|
115
|
+
"BrokerHealthResult",
|
|
116
|
+
"CircuitBreakerMiddleware",
|
|
117
|
+
"CircuitBreakerOpenError",
|
|
118
|
+
"ClassifiedError",
|
|
119
|
+
"CompressionConfig",
|
|
120
|
+
"ConfigurationError",
|
|
121
|
+
"ConnectionConfig",
|
|
122
|
+
"ConsumerConfig",
|
|
123
|
+
"Context",
|
|
124
|
+
"ContextRepo",
|
|
125
|
+
"DIResolver",
|
|
126
|
+
"DLQInspector",
|
|
127
|
+
"DataclassDecoder",
|
|
128
|
+
"DeduplicationConfig",
|
|
129
|
+
"DeduplicationMarkPolicy",
|
|
130
|
+
"DeduplicationMiddleware",
|
|
131
|
+
"DependencyScope",
|
|
132
|
+
"Depends",
|
|
133
|
+
"ErrorSeverity",
|
|
134
|
+
"ExchangeType",
|
|
135
|
+
"FlowController",
|
|
136
|
+
"Header",
|
|
137
|
+
"HealthCheckConfig",
|
|
138
|
+
"HealthStatus",
|
|
139
|
+
"HealthWatcher",
|
|
140
|
+
"JsonParser",
|
|
141
|
+
"LoggingConfig",
|
|
142
|
+
"ManagementConfig",
|
|
143
|
+
"MessageDecoder",
|
|
144
|
+
"MessageEnvelope",
|
|
145
|
+
"MessageParser",
|
|
146
|
+
"MetricsCollector",
|
|
147
|
+
"MetricsConfig",
|
|
148
|
+
"MetricsMiddleware",
|
|
149
|
+
"MissingDependencyError",
|
|
150
|
+
"NackMessage",
|
|
151
|
+
"OTelTracingMiddleware",
|
|
152
|
+
"Path",
|
|
153
|
+
"PoolConfig",
|
|
154
|
+
"PrometheusCollector",
|
|
155
|
+
"PublishOutcome",
|
|
156
|
+
"PublishStatus",
|
|
157
|
+
"PublisherConfig",
|
|
158
|
+
"PydanticDecoder",
|
|
159
|
+
"QueueMetricsPoller",
|
|
160
|
+
"QueueType",
|
|
161
|
+
"RabbitApp",
|
|
162
|
+
"RabbitConfig",
|
|
163
|
+
"RabbitExchange",
|
|
164
|
+
"RabbitManagementClient",
|
|
165
|
+
"RabbitMessage",
|
|
166
|
+
"RabbitQueue",
|
|
167
|
+
"RabbitRouter",
|
|
168
|
+
"RateLimitConfig",
|
|
169
|
+
"RateLimitMiddleware",
|
|
170
|
+
"RawDecoder",
|
|
171
|
+
"RejectMessage",
|
|
172
|
+
"RejectWithoutDLXPolicy",
|
|
173
|
+
"ReplayResult",
|
|
174
|
+
"RetryConfig",
|
|
175
|
+
"RetryDisabled",
|
|
176
|
+
"SSLConfig",
|
|
177
|
+
"SafetyConfig",
|
|
178
|
+
"SecurityConfig",
|
|
179
|
+
"SerializationPipeline",
|
|
180
|
+
"SocketConfig",
|
|
181
|
+
"SyncBatchPublisher",
|
|
182
|
+
"SyncBroker",
|
|
183
|
+
"SyncWorkerPool",
|
|
184
|
+
"TopologyMode",
|
|
185
|
+
"UnsafeTopologyError",
|
|
186
|
+
"WorkerConfig",
|
|
187
|
+
"__version__",
|
|
188
|
+
"broker_health_check",
|
|
189
|
+
"broker_health_check_async",
|
|
190
|
+
"broker_liveness",
|
|
191
|
+
"broker_liveness_async",
|
|
192
|
+
"broker_readiness",
|
|
193
|
+
"broker_readiness_async",
|
|
194
|
+
"configure_structlog",
|
|
195
|
+
"experimental",
|
|
196
|
+
"generate_asyncapi_doc",
|
|
197
|
+
"generate_asyncapi_json",
|
|
198
|
+
"metrics_app",
|
|
199
|
+
"rabbitkit_lifespan",
|
|
200
|
+
"start_metrics_server",
|
|
201
|
+
]
|
rabbitkit/_version.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""rabbitkit.aio — deprecated alias for ``rabbitkit.async_`` (L8).
|
|
2
|
+
|
|
3
|
+
``rabbitkit.async_`` is the canonical import path (it matches the actual
|
|
4
|
+
module structure — ``async_/broker.py``, ``async_/transport.py``, etc. — and
|
|
5
|
+
is what every example, doc, and test in this codebase uses). This module is
|
|
6
|
+
kept only so any existing ``from rabbitkit.aio import ...`` import keeps
|
|
7
|
+
working; importing it emits a ``DeprecationWarning``.
|
|
8
|
+
|
|
9
|
+
Prefer::
|
|
10
|
+
|
|
11
|
+
from rabbitkit.async_.broker import AsyncBroker
|
|
12
|
+
# or, once exported at top level:
|
|
13
|
+
from rabbitkit import AsyncBroker
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
from rabbitkit.async_.broker import AsyncBroker
|
|
19
|
+
from rabbitkit.async_.transport import AsyncTransportImpl
|
|
20
|
+
|
|
21
|
+
warnings.warn(
|
|
22
|
+
"rabbitkit.aio is deprecated -- import from rabbitkit.async_ (or rabbitkit) instead. "
|
|
23
|
+
"rabbitkit.async_ is the canonical async import path.",
|
|
24
|
+
DeprecationWarning,
|
|
25
|
+
stacklevel=2,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AsyncBroker",
|
|
30
|
+
"AsyncTransportImpl",
|
|
31
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""BatchPublisher — transparent batch publish with amortized confirm wait.
|
|
2
|
+
|
|
3
|
+
Collects messages from concurrent callers into fixed-size batches, then
|
|
4
|
+
publishes the whole batch on a single pooled channel. The broker-side confirm
|
|
5
|
+
wait is paid once per batch rather than once per message, dramatically reducing
|
|
6
|
+
the per-message cost at high concurrency.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from rabbitkit.core.config import BatchPublishConfig
|
|
11
|
+
|
|
12
|
+
broker = AsyncBroker(
|
|
13
|
+
config,
|
|
14
|
+
batch_config=BatchPublishConfig(batch_size=64, flush_interval_ms=20),
|
|
15
|
+
)
|
|
16
|
+
await broker.start()
|
|
17
|
+
# broker.publish() is now transparently batched
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import contextlib
|
|
24
|
+
import logging
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
from rabbitkit.core.config import BatchPublishConfig
|
|
28
|
+
from rabbitkit.core.types import MessageEnvelope, PublishOutcome, PublishStatus
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from rabbitkit.async_.transport import AsyncTransportImpl
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncBatchPublisher:
|
|
37
|
+
"""Transparent batch-publish wrapper for AsyncTransportImpl.
|
|
38
|
+
|
|
39
|
+
N concurrent ``broker.publish()`` calls are coalesced into one batch,
|
|
40
|
+
published on a single channel, and their confirms are gathered together.
|
|
41
|
+
Reduces per-message pool acquire/release and amortizes confirm round-trips.
|
|
42
|
+
|
|
43
|
+
The caller's coroutine blocks until its message is included in a flushed
|
|
44
|
+
batch and the confirm resolves — semantics are identical to direct publish.
|
|
45
|
+
|
|
46
|
+
Unlike ``highload.batch.BatchPublisher`` (a timing/buffering helper that
|
|
47
|
+
publishes each message individually), this class pipelines confirms: all
|
|
48
|
+
messages in a batch share one channel and their ACKs are awaited together.
|
|
49
|
+
|
|
50
|
+
Blast-radius note (M17): because a batch shares one channel, an
|
|
51
|
+
AMQP-channel-level failure (e.g. a confirm timeout that closes the channel)
|
|
52
|
+
fails EVERY in-flight publish in that batch — each caller's future is
|
|
53
|
+
settled with the error (never silently lost), and the channel is replaced
|
|
54
|
+
before the next batch, but a single slow confirm can amplify into
|
|
55
|
+
batch-wide errors. This is inherent to sharing a channel for pipelined
|
|
56
|
+
confirms; there is no per-message isolation without giving up batching. To
|
|
57
|
+
bound the blast radius, lower ``BatchPublishConfig.batch_size`` (fewer
|
|
58
|
+
siblings share a channel) — or use direct ``broker.publish()`` for
|
|
59
|
+
publishes that must fail independently.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, transport: AsyncTransportImpl, config: BatchPublishConfig) -> None:
|
|
63
|
+
self._transport = transport
|
|
64
|
+
self._config = config
|
|
65
|
+
self._pending: asyncio.Queue[tuple[MessageEnvelope, asyncio.Future[PublishOutcome]]] = (
|
|
66
|
+
asyncio.Queue(maxsize=config.max_in_flight)
|
|
67
|
+
)
|
|
68
|
+
self._flush_tasks: list[asyncio.Task[None]] = []
|
|
69
|
+
|
|
70
|
+
def _worker_count(self) -> int:
|
|
71
|
+
if self._config.flush_workers > 0:
|
|
72
|
+
return self._config.flush_workers
|
|
73
|
+
return min(16, max(1, self._config.max_in_flight // self._config.batch_size))
|
|
74
|
+
|
|
75
|
+
async def start(self) -> None:
|
|
76
|
+
"""Start N concurrent flush loops (one per channel slot)."""
|
|
77
|
+
n = self._worker_count()
|
|
78
|
+
self._flush_tasks = [
|
|
79
|
+
asyncio.create_task(self._flush_loop(), name=f"rabbitkit.batch-flush-{i}")
|
|
80
|
+
for i in range(n)
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
async def stop(self) -> None:
|
|
84
|
+
"""Cancel all flush loops and drain any remaining queued messages."""
|
|
85
|
+
for task in self._flush_tasks:
|
|
86
|
+
task.cancel()
|
|
87
|
+
for task in self._flush_tasks:
|
|
88
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
89
|
+
await task
|
|
90
|
+
self._flush_tasks = []
|
|
91
|
+
while not self._pending.empty():
|
|
92
|
+
try:
|
|
93
|
+
_, fut = self._pending.get_nowait()
|
|
94
|
+
if not fut.done():
|
|
95
|
+
fut.set_exception(RuntimeError("BatchPublisher stopped before flush"))
|
|
96
|
+
except asyncio.QueueEmpty: # pragma: no cover — TOCTOU guard, unreachable in asyncio
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
async def publish(self, envelope: MessageEnvelope) -> PublishOutcome:
|
|
100
|
+
"""Enqueue *envelope* and wait for it to be included in a batch flush."""
|
|
101
|
+
loop = asyncio.get_running_loop()
|
|
102
|
+
fut: asyncio.Future[PublishOutcome] = loop.create_future()
|
|
103
|
+
await self._pending.put((envelope, fut))
|
|
104
|
+
return await fut
|
|
105
|
+
|
|
106
|
+
async def _flush_loop(self) -> None:
|
|
107
|
+
"""Each worker holds one channel for its lifetime — no acquire/release per batch."""
|
|
108
|
+
interval = self._config.flush_interval_ms / 1000.0
|
|
109
|
+
channel: Any = None
|
|
110
|
+
batch: list[tuple[MessageEnvelope, asyncio.Future[PublishOutcome]]] = []
|
|
111
|
+
try:
|
|
112
|
+
channel = await self._transport._conn_pool.acquire_publisher_channel()
|
|
113
|
+
while True:
|
|
114
|
+
batch = []
|
|
115
|
+
|
|
116
|
+
# Block until the first item arrives
|
|
117
|
+
batch.append(await self._pending.get())
|
|
118
|
+
|
|
119
|
+
# Fast drain: grab all immediately-available items without yielding.
|
|
120
|
+
# At high concurrency the queue is almost always non-empty here, so
|
|
121
|
+
# this avoids the coroutine/timeout overhead of repeated wait_for calls.
|
|
122
|
+
while len(batch) < self._config.batch_size:
|
|
123
|
+
try:
|
|
124
|
+
batch.append(self._pending.get_nowait())
|
|
125
|
+
except asyncio.QueueEmpty:
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
# If still under batch_size, wait briefly for stragglers
|
|
129
|
+
if len(batch) < self._config.batch_size:
|
|
130
|
+
loop = asyncio.get_running_loop()
|
|
131
|
+
deadline = loop.time() + interval
|
|
132
|
+
while len(batch) < self._config.batch_size:
|
|
133
|
+
remaining = deadline - loop.time()
|
|
134
|
+
if remaining <= 0:
|
|
135
|
+
break
|
|
136
|
+
try:
|
|
137
|
+
async with asyncio.timeout(remaining):
|
|
138
|
+
batch.append(await self._pending.get())
|
|
139
|
+
except TimeoutError:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
await self._flush(channel, batch)
|
|
144
|
+
except BaseException as exc:
|
|
145
|
+
# Settle any futures _flush didn't resolve (including on
|
|
146
|
+
# CancelledError). Convert CancelledError to RuntimeError so
|
|
147
|
+
# it can be set on the Future (set_exception rejects BaseException).
|
|
148
|
+
err: BaseException = (
|
|
149
|
+
exc if isinstance(exc, Exception) else RuntimeError("Batch publisher cancelled")
|
|
150
|
+
)
|
|
151
|
+
for _, fut in batch:
|
|
152
|
+
if not fut.done():
|
|
153
|
+
fut.set_exception(err)
|
|
154
|
+
if isinstance(exc, Exception):
|
|
155
|
+
# Channel error — replace it. Set channel=None first so
|
|
156
|
+
# the finally block doesn't double-release if acquire fails.
|
|
157
|
+
old, channel = channel, None
|
|
158
|
+
with contextlib.suppress(Exception):
|
|
159
|
+
await self._transport._conn_pool.release_publisher_channel(old)
|
|
160
|
+
channel = await self._transport._conn_pool.acquire_publisher_channel()
|
|
161
|
+
else:
|
|
162
|
+
raise # CancelledError must propagate
|
|
163
|
+
else:
|
|
164
|
+
# _publish_on_channel closes the channel on confirm timeout;
|
|
165
|
+
# detect that and replace it before the next batch.
|
|
166
|
+
if channel.is_closed:
|
|
167
|
+
old, channel = channel, None
|
|
168
|
+
with contextlib.suppress(Exception):
|
|
169
|
+
await self._transport._conn_pool.release_publisher_channel(old)
|
|
170
|
+
channel = await self._transport._conn_pool.acquire_publisher_channel()
|
|
171
|
+
except BaseException:
|
|
172
|
+
# Any exception escaping the loop (e.g. CancelledError at pending.get()
|
|
173
|
+
# or straggler wait) — settle any dequeued-but-unresolved futures.
|
|
174
|
+
for _, fut in batch:
|
|
175
|
+
if not fut.done():
|
|
176
|
+
fut.set_exception(RuntimeError("Batch publisher cancelled"))
|
|
177
|
+
raise
|
|
178
|
+
finally:
|
|
179
|
+
if channel is not None:
|
|
180
|
+
with contextlib.suppress(Exception):
|
|
181
|
+
await self._transport._conn_pool.release_publisher_channel(channel)
|
|
182
|
+
|
|
183
|
+
async def _flush(
|
|
184
|
+
self,
|
|
185
|
+
channel: Any,
|
|
186
|
+
batch: list[tuple[MessageEnvelope, asyncio.Future[PublishOutcome]]],
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Publish all envelopes on the worker's persistent channel and resolve futures."""
|
|
189
|
+
outcomes: list[Any] = await asyncio.gather(
|
|
190
|
+
*[self._transport._publish_on_channel(channel, env) for env, _ in batch],
|
|
191
|
+
return_exceptions=True,
|
|
192
|
+
)
|
|
193
|
+
for (_, fut), outcome in zip(batch, outcomes, strict=False):
|
|
194
|
+
if not fut.done():
|
|
195
|
+
if isinstance(outcome, BaseException):
|
|
196
|
+
fut.set_exception(outcome)
|
|
197
|
+
else:
|
|
198
|
+
fut.set_result(outcome)
|
|
199
|
+
|
|
200
|
+
# M17: _publish_on_channel no longer closes the channel itself on a
|
|
201
|
+
# confirm timeout (that used to fire from INSIDE one of the concurrent
|
|
202
|
+
# calls above, corrupting sibling in-flight confirms sharing this same
|
|
203
|
+
# channel). Instead, close it HERE — every concurrent publish in this
|
|
204
|
+
# batch has already resolved by this point, so closing can no longer
|
|
205
|
+
# amplify this batch's failure onto a message that would have
|
|
206
|
+
# confirmed fine. The flush loop's post-_flush check (channel.is_closed)
|
|
207
|
+
# still replaces it for the next batch, preserving the original
|
|
208
|
+
# "don't reuse a wedged channel" protection.
|
|
209
|
+
if not channel.is_closed and any(
|
|
210
|
+
not isinstance(o, BaseException) and o.status == PublishStatus.TIMEOUT for o in outcomes
|
|
211
|
+
):
|
|
212
|
+
with contextlib.suppress(Exception):
|
|
213
|
+
await channel.close()
|