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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Environment-variable driven RabbitMQ configuration.
|
|
2
|
+
|
|
3
|
+
Requires: ``pip install rabbitkit[settings]``
|
|
4
|
+
|
|
5
|
+
Maps ``RABBITMQ_*`` environment variables to ``RabbitConfig`` using
|
|
6
|
+
pydantic-settings ``BaseSettings``. Supports ``.env`` files, environment
|
|
7
|
+
overrides, and type coercion out of the box.
|
|
8
|
+
|
|
9
|
+
Lazy import — no ``ImportError`` at import time when pydantic-settings is
|
|
10
|
+
absent; a placeholder class is installed instead and raises on instantiation.
|
|
11
|
+
|
|
12
|
+
Quick start
|
|
13
|
+
-----------
|
|
14
|
+
# .env (or set as real environment variables)
|
|
15
|
+
RABBITMQ_HOST=rabbitmq.prod.internal
|
|
16
|
+
RABBITMQ_PORT=5672
|
|
17
|
+
RABBITMQ_USER=myapp
|
|
18
|
+
RABBITMQ_PASSWORD=secret
|
|
19
|
+
RABBITMQ_VHOST=/production
|
|
20
|
+
RABBITMQ_PREFETCH_COUNT=20
|
|
21
|
+
RABBITMQ_TOPOLOGY_MODE=AUTO_DECLARE
|
|
22
|
+
|
|
23
|
+
# application code
|
|
24
|
+
from rabbitkit.core.env_config import RabbitSettings
|
|
25
|
+
from rabbitkit.async_ import AsyncBroker
|
|
26
|
+
|
|
27
|
+
settings = RabbitSettings() # reads from env / .env automatically
|
|
28
|
+
config = settings.to_rabbit_config()
|
|
29
|
+
broker = AsyncBroker(config)
|
|
30
|
+
|
|
31
|
+
Supported environment variables
|
|
32
|
+
--------------------------------
|
|
33
|
+
All variables are prefixed with ``RABBITMQ_`` and case-insensitive:
|
|
34
|
+
|
|
35
|
+
Connection
|
|
36
|
+
RABBITMQ_HOST (str, default "localhost")
|
|
37
|
+
RABBITMQ_PORT (int, default 5672)
|
|
38
|
+
RABBITMQ_USER (str, default "guest")
|
|
39
|
+
RABBITMQ_PASSWORD (str, default "guest")
|
|
40
|
+
RABBITMQ_VHOST (str, default "/")
|
|
41
|
+
RABBITMQ_HEARTBEAT (int, default 30)
|
|
42
|
+
RABBITMQ_SOCKET_TIMEOUT (float, default 10.0)
|
|
43
|
+
RABBITMQ_BLOCKED_CONNECTION_TIMEOUT (float, default 60.0)
|
|
44
|
+
RABBITMQ_CONNECTION_NAME (str | None, default None)
|
|
45
|
+
RABBITMQ_RECONNECT_BACKOFF_BASE (float, default 1.0)
|
|
46
|
+
RABBITMQ_RECONNECT_BACKOFF_MAX (float, default 30.0)
|
|
47
|
+
|
|
48
|
+
Consumer
|
|
49
|
+
RABBITMQ_PREFETCH_COUNT (int, default 10)
|
|
50
|
+
RABBITMQ_GRACEFUL_TIMEOUT (float, default 30.0)
|
|
51
|
+
|
|
52
|
+
Publisher
|
|
53
|
+
RABBITMQ_CONFIRM_DELIVERY (bool, default True)
|
|
54
|
+
RABBITMQ_CONFIRM_TIMEOUT (float, default 5.0)
|
|
55
|
+
RABBITMQ_MANDATORY (bool, default False)
|
|
56
|
+
RABBITMQ_PERSISTENT (bool, default True)
|
|
57
|
+
RABBITMQ_DEFAULT_EXCHANGE (str, default "")
|
|
58
|
+
|
|
59
|
+
Pool
|
|
60
|
+
RABBITMQ_CHANNEL_POOL_SIZE (int, default 10)
|
|
61
|
+
RABBITMQ_PUBLISHER_CONNECTIONS (int, default 1)
|
|
62
|
+
RABBITMQ_CONSUMER_CONNECTIONS (int, default 1)
|
|
63
|
+
RABBITMQ_CHANNEL_ACQUIRE_TIMEOUT (float, default 10.0)
|
|
64
|
+
|
|
65
|
+
SSL
|
|
66
|
+
RABBITMQ_SSL_ENABLED (bool, default False)
|
|
67
|
+
RABBITMQ_SSL_CERTFILE (str | None, default None)
|
|
68
|
+
RABBITMQ_SSL_KEYFILE (str | None, default None)
|
|
69
|
+
RABBITMQ_SSL_CA_CERTS (str | None, default None)
|
|
70
|
+
RABBITMQ_SSL_SERVER_HOSTNAME (str | None, default None)
|
|
71
|
+
|
|
72
|
+
Retry (set RABBITMQ_RETRY_MAX_RETRIES > 0 to enable)
|
|
73
|
+
RABBITMQ_RETRY_MAX_RETRIES (int, default 0 — disabled)
|
|
74
|
+
RABBITMQ_RETRY_DELAYS (str, default "5,30,120,600" — comma-separated ints)
|
|
75
|
+
RABBITMQ_RETRY_JITTER_FACTOR (float, default 0.1)
|
|
76
|
+
|
|
77
|
+
Topology
|
|
78
|
+
RABBITMQ_TOPOLOGY_MODE (str, default "AUTO_DECLARE")
|
|
79
|
+
valid: AUTO_DECLARE, PASSIVE_ONLY, MANUAL
|
|
80
|
+
|
|
81
|
+
Override at runtime
|
|
82
|
+
-------------------
|
|
83
|
+
pydantic-settings respects constructor keyword arguments, so you can mix
|
|
84
|
+
env-file defaults with runtime overrides:
|
|
85
|
+
|
|
86
|
+
settings = RabbitSettings(host="staging-rabbit", prefetch_count=5)
|
|
87
|
+
config = settings.to_rabbit_config()
|
|
88
|
+
|
|
89
|
+
Checking availability
|
|
90
|
+
---------------------
|
|
91
|
+
from rabbitkit.core.env_config import _PYDANTIC_SETTINGS_AVAILABLE
|
|
92
|
+
|
|
93
|
+
if _PYDANTIC_SETTINGS_AVAILABLE:
|
|
94
|
+
settings = RabbitSettings()
|
|
95
|
+
else:
|
|
96
|
+
config = RabbitConfig(connection=ConnectionConfig(...))
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
from __future__ import annotations
|
|
100
|
+
|
|
101
|
+
from typing import Any
|
|
102
|
+
|
|
103
|
+
_PYDANTIC_SETTINGS_AVAILABLE = False
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
from pydantic import Field, SecretStr
|
|
107
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
108
|
+
|
|
109
|
+
_PYDANTIC_SETTINGS_AVAILABLE = True
|
|
110
|
+
except ImportError: # pragma: no cover
|
|
111
|
+
pass # pragma: no cover
|
|
112
|
+
|
|
113
|
+
if _PYDANTIC_SETTINGS_AVAILABLE:
|
|
114
|
+
from rabbitkit.core.config import (
|
|
115
|
+
ConnectionConfig,
|
|
116
|
+
ConsumerConfig,
|
|
117
|
+
PoolConfig,
|
|
118
|
+
PublisherConfig,
|
|
119
|
+
RabbitConfig,
|
|
120
|
+
RetryConfig,
|
|
121
|
+
SecurityConfig,
|
|
122
|
+
SSLConfig,
|
|
123
|
+
)
|
|
124
|
+
from rabbitkit.core.types import TopologyMode
|
|
125
|
+
|
|
126
|
+
class RabbitSettings(BaseSettings):
|
|
127
|
+
"""Load RabbitMQ config from RABBITMQ_* environment variables.
|
|
128
|
+
|
|
129
|
+
Example::
|
|
130
|
+
|
|
131
|
+
settings = RabbitSettings() # reads from env / .env
|
|
132
|
+
config = settings.to_rabbit_config()
|
|
133
|
+
broker = AsyncBroker(config)
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
model_config = SettingsConfigDict(
|
|
137
|
+
env_prefix="RABBITMQ_",
|
|
138
|
+
case_sensitive=False,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Connection
|
|
142
|
+
host: str = "localhost"
|
|
143
|
+
port: int = 5672
|
|
144
|
+
user: str = Field(default="guest")
|
|
145
|
+
password: SecretStr = SecretStr("guest")
|
|
146
|
+
vhost: str = "/"
|
|
147
|
+
heartbeat: int = 30
|
|
148
|
+
socket_timeout: float = 10.0
|
|
149
|
+
blocked_connection_timeout: float = 60.0
|
|
150
|
+
connection_name: str | None = None
|
|
151
|
+
reconnect_backoff_base: float = 1.0
|
|
152
|
+
reconnect_backoff_max: float = 30.0
|
|
153
|
+
|
|
154
|
+
# Consumer
|
|
155
|
+
prefetch_count: int = 10
|
|
156
|
+
graceful_timeout: float = 30.0
|
|
157
|
+
|
|
158
|
+
# Publisher
|
|
159
|
+
confirm_delivery: bool = True
|
|
160
|
+
confirm_timeout: float = 5.0
|
|
161
|
+
mandatory: bool = False
|
|
162
|
+
persistent: bool = True
|
|
163
|
+
default_exchange: str = ""
|
|
164
|
+
|
|
165
|
+
# Pool
|
|
166
|
+
channel_pool_size: int = 10
|
|
167
|
+
publisher_connections: int = 1
|
|
168
|
+
consumer_connections: int = 1
|
|
169
|
+
channel_acquire_timeout: float = 10.0
|
|
170
|
+
|
|
171
|
+
# SSL
|
|
172
|
+
ssl_enabled: bool = False
|
|
173
|
+
ssl_certfile: str | None = None
|
|
174
|
+
ssl_keyfile: str | None = None
|
|
175
|
+
ssl_ca_certs: str | None = None
|
|
176
|
+
ssl_server_hostname: str | None = None
|
|
177
|
+
|
|
178
|
+
# Retry (0 = disabled, no RetryConfig created)
|
|
179
|
+
retry_max_retries: int = 0
|
|
180
|
+
retry_delays: str = "5,30,120,600"
|
|
181
|
+
retry_jitter_factor: float = 0.1
|
|
182
|
+
|
|
183
|
+
# Topology
|
|
184
|
+
topology_mode: str = "AUTO_DECLARE"
|
|
185
|
+
|
|
186
|
+
def to_rabbit_config(self) -> RabbitConfig:
|
|
187
|
+
"""Convert to a RabbitConfig dataclass."""
|
|
188
|
+
ssl = SSLConfig(
|
|
189
|
+
enabled=self.ssl_enabled,
|
|
190
|
+
certfile=self.ssl_certfile,
|
|
191
|
+
keyfile=self.ssl_keyfile,
|
|
192
|
+
ca_certs=self.ssl_ca_certs,
|
|
193
|
+
server_hostname=self.ssl_server_hostname,
|
|
194
|
+
)
|
|
195
|
+
retry: RetryConfig | None = None
|
|
196
|
+
if self.retry_max_retries > 0:
|
|
197
|
+
delays = tuple(int(d) for d in self.retry_delays.split(",") if d.strip())
|
|
198
|
+
retry = RetryConfig(
|
|
199
|
+
max_retries=self.retry_max_retries,
|
|
200
|
+
delays=delays,
|
|
201
|
+
jitter_factor=self.retry_jitter_factor,
|
|
202
|
+
)
|
|
203
|
+
return RabbitConfig(
|
|
204
|
+
connection=ConnectionConfig(
|
|
205
|
+
host=self.host,
|
|
206
|
+
port=self.port,
|
|
207
|
+
username=self.user,
|
|
208
|
+
password=self.password.get_secret_value(),
|
|
209
|
+
vhost=self.vhost,
|
|
210
|
+
heartbeat=self.heartbeat,
|
|
211
|
+
socket_timeout=self.socket_timeout,
|
|
212
|
+
blocked_connection_timeout=self.blocked_connection_timeout,
|
|
213
|
+
connection_name=self.connection_name,
|
|
214
|
+
reconnect_backoff_base=self.reconnect_backoff_base,
|
|
215
|
+
reconnect_backoff_max=self.reconnect_backoff_max,
|
|
216
|
+
),
|
|
217
|
+
security=SecurityConfig(ssl=ssl),
|
|
218
|
+
consumer=ConsumerConfig(
|
|
219
|
+
prefetch_count=self.prefetch_count,
|
|
220
|
+
graceful_timeout=self.graceful_timeout,
|
|
221
|
+
),
|
|
222
|
+
publisher=PublisherConfig(
|
|
223
|
+
confirm_delivery=self.confirm_delivery,
|
|
224
|
+
confirm_timeout=self.confirm_timeout,
|
|
225
|
+
mandatory=self.mandatory,
|
|
226
|
+
persistent=self.persistent,
|
|
227
|
+
exchange=self.default_exchange,
|
|
228
|
+
),
|
|
229
|
+
pool=PoolConfig(
|
|
230
|
+
channel_pool_size=self.channel_pool_size,
|
|
231
|
+
publisher_connections=self.publisher_connections,
|
|
232
|
+
consumer_connections=self.consumer_connections,
|
|
233
|
+
channel_acquire_timeout=self.channel_acquire_timeout,
|
|
234
|
+
),
|
|
235
|
+
topology_mode=TopologyMode[self.topology_mode.upper()],
|
|
236
|
+
retry=retry,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
else: # pragma: no cover
|
|
240
|
+
|
|
241
|
+
class RabbitSettings: # type: ignore[no-redef] # pragma: no cover
|
|
242
|
+
"""Placeholder — pydantic-settings not installed."""
|
|
243
|
+
|
|
244
|
+
def __init__(self, **_: Any) -> None:
|
|
245
|
+
raise ImportError(
|
|
246
|
+
"RabbitSettings requires pydantic-settings. Install with: pip install rabbitkit[settings]"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def to_rabbit_config(self) -> Any:
|
|
250
|
+
"""Not available without pydantic-settings."""
|
|
251
|
+
raise ImportError("pydantic-settings not installed")
|
rabbitkit/core/errors.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Error classification — transport-agnostic.
|
|
2
|
+
|
|
3
|
+
Transport-specific exception tuples (pika.exceptions.StreamLostError, etc.)
|
|
4
|
+
live in sync/connection.py and async_/connection.py, NOT here.
|
|
5
|
+
|
|
6
|
+
This module provides:
|
|
7
|
+
- Generic stdlib exception tuples for classification
|
|
8
|
+
- Pluggable predicate-based classification
|
|
9
|
+
- Configurable unknown_policy (default=PERMANENT)
|
|
10
|
+
|
|
11
|
+
See Contract 7 in the plan for evaluation order and rationale.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from collections.abc import Callable, Sequence
|
|
18
|
+
|
|
19
|
+
from rabbitkit.core.types import ClassifiedError, ErrorSeverity
|
|
20
|
+
|
|
21
|
+
# ── Missing DI dependency (H10) ──────────────────────────────────────────
|
|
22
|
+
# Defined before PERMANENT_ERRORS below so it can be listed there directly.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MissingDependencyError(Exception):
|
|
26
|
+
"""Raised at message-processing time when a required ``Header()`` /
|
|
27
|
+
``Path()`` / ``Context()`` marker's value is absent from the incoming
|
|
28
|
+
message and no default is available — neither on the marker itself
|
|
29
|
+
(``Header("x-tenant", default=...)``) nor the handler's own parameter
|
|
30
|
+
default (``tenant: Annotated[str | None, Header("x-tenant")] = None``).
|
|
31
|
+
|
|
32
|
+
Names the specific parameter and marker so the failure is immediately
|
|
33
|
+
actionable — unlike the bare ``KeyError`` this replaces, which looked
|
|
34
|
+
identical to a handler bug (e.g. indexing a dict) and gave no indication
|
|
35
|
+
which DI marker was the culprit. Classified PERMANENT by
|
|
36
|
+
:func:`classify_error` (see ``PERMANENT_ERRORS`` below), matching the
|
|
37
|
+
``KeyError`` classification it replaces: a missing required value means
|
|
38
|
+
the message itself is malformed for this handler — retrying will not fix
|
|
39
|
+
it, so it settles straight to the DLQ rather than looping.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, marker_repr: str, param_name: str) -> None:
|
|
43
|
+
super().__init__(
|
|
44
|
+
f"{marker_repr} for parameter {param_name!r} is required but missing from the "
|
|
45
|
+
"message, and no default is available (neither on the marker itself nor as a "
|
|
46
|
+
"Python parameter default)."
|
|
47
|
+
)
|
|
48
|
+
self.param_name = param_name
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Generic (stdlib) error categories — transport layers extend these
|
|
52
|
+
TRANSIENT_ERRORS: tuple[type[BaseException], ...] = (
|
|
53
|
+
TimeoutError,
|
|
54
|
+
EOFError, # NOT an OSError subclass — must be listed explicitly
|
|
55
|
+
OSError, # covers ConnectionResetError, BrokenPipeError, ConnectionAbortedError
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
PERMANENT_ERRORS: tuple[type[BaseException], ...] = (
|
|
59
|
+
json.JSONDecodeError,
|
|
60
|
+
KeyError,
|
|
61
|
+
ValueError,
|
|
62
|
+
TypeError,
|
|
63
|
+
UnicodeDecodeError,
|
|
64
|
+
AttributeError,
|
|
65
|
+
MissingDependencyError,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Type alias for error predicates
|
|
69
|
+
ErrorPredicate = Callable[[BaseException], bool | None]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def classify_error(
|
|
73
|
+
exc: BaseException,
|
|
74
|
+
*,
|
|
75
|
+
predicates: Sequence[ErrorPredicate] = (),
|
|
76
|
+
transient: tuple[type[BaseException], ...] = TRANSIENT_ERRORS,
|
|
77
|
+
permanent: tuple[type[BaseException], ...] = PERMANENT_ERRORS,
|
|
78
|
+
unknown_policy: ErrorSeverity = ErrorSeverity.PERMANENT,
|
|
79
|
+
) -> ClassifiedError:
|
|
80
|
+
"""Classify exception severity.
|
|
81
|
+
|
|
82
|
+
Evaluation order:
|
|
83
|
+
1. User predicates (first non-None wins)
|
|
84
|
+
2. transient tuple (isinstance check)
|
|
85
|
+
3. permanent tuple (isinstance check)
|
|
86
|
+
4. unknown_policy (configurable, default=PERMANENT)
|
|
87
|
+
|
|
88
|
+
DEFAULT IS PERMANENT, NOT TRANSIENT.
|
|
89
|
+
Reason: unknown errors include malformed payloads, business validation
|
|
90
|
+
failures, handler bugs, and schema mismatches. Treating these as
|
|
91
|
+
transient creates retry storms. Network/connection errors are already
|
|
92
|
+
in the transient tuple.
|
|
93
|
+
|
|
94
|
+
Override per-route: RetryMiddleware accepts unknown_policy to change
|
|
95
|
+
per route.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
exc: The exception to classify.
|
|
99
|
+
predicates: User-provided classification functions.
|
|
100
|
+
Return True=transient, False=permanent, None=no opinion.
|
|
101
|
+
transient: Exception types considered transient.
|
|
102
|
+
permanent: Exception types considered permanent.
|
|
103
|
+
unknown_policy: Severity for unclassified exceptions.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
ClassifiedError with severity, original exception, and reason.
|
|
107
|
+
"""
|
|
108
|
+
# 1. User predicates (first non-None wins)
|
|
109
|
+
for predicate in predicates:
|
|
110
|
+
result = predicate(exc)
|
|
111
|
+
if result is True:
|
|
112
|
+
return ClassifiedError(
|
|
113
|
+
severity=ErrorSeverity.TRANSIENT,
|
|
114
|
+
original=exc,
|
|
115
|
+
reason=f"predicate classified as transient: {type(exc).__name__}",
|
|
116
|
+
)
|
|
117
|
+
if result is False:
|
|
118
|
+
return ClassifiedError(
|
|
119
|
+
severity=ErrorSeverity.PERMANENT,
|
|
120
|
+
original=exc,
|
|
121
|
+
reason=f"predicate classified as permanent: {type(exc).__name__}",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# 2. Transient tuple (isinstance check)
|
|
125
|
+
if isinstance(exc, transient):
|
|
126
|
+
return ClassifiedError(
|
|
127
|
+
severity=ErrorSeverity.TRANSIENT,
|
|
128
|
+
original=exc,
|
|
129
|
+
reason=f"transient error: {type(exc).__name__}",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 3. Permanent tuple (isinstance check)
|
|
133
|
+
if isinstance(exc, permanent):
|
|
134
|
+
return ClassifiedError(
|
|
135
|
+
severity=ErrorSeverity.PERMANENT,
|
|
136
|
+
original=exc,
|
|
137
|
+
reason=f"permanent error: {type(exc).__name__}",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# 4. Unknown policy
|
|
141
|
+
return ClassifiedError(
|
|
142
|
+
severity=unknown_policy,
|
|
143
|
+
original=exc,
|
|
144
|
+
reason=f"unknown error classified as {unknown_policy.value}: {type(exc).__name__}",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── Configuration error (single canonical location) ─────────────────────
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ConfigurationError(Exception):
|
|
152
|
+
"""Raised for invalid configuration detected at registration time.
|
|
153
|
+
|
|
154
|
+
Single canonical class for all registration-time misconfigurations (route
|
|
155
|
+
conflicts, invalid handler signatures, bad retry/ack combinations). Both
|
|
156
|
+
``core/route.py`` and ``di/resolver.py`` raise this; tests/users can catch
|
|
157
|
+
one type regardless of import source.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ── Unsafe topology error ────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class UnsafeTopologyError(ConfigurationError):
|
|
165
|
+
"""Raised at startup when ``RejectWithoutDLXPolicy.ERROR`` is active and a
|
|
166
|
+
route can ``reject(requeue=False)`` but its queue has no dead-letter
|
|
167
|
+
exchange — RabbitMQ would silently discard rejected messages.
|
|
168
|
+
|
|
169
|
+
Subclasses :class:`ConfigurationError` so existing catch-alls for
|
|
170
|
+
registration/startup misconfiguration keep working.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ── Publish error (opt-in) ───────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class PublishError(Exception):
|
|
178
|
+
"""Raised by ``PublishOutcome.raise_for_status()`` on a failed publish.
|
|
179
|
+
|
|
180
|
+
``broker.publish()`` never raises on its own — it returns a
|
|
181
|
+
``PublishOutcome`` so callers can decide how to handle NACKED / TIMEOUT /
|
|
182
|
+
RETURNED / ERROR. Code that prefers exceptions can opt in with
|
|
183
|
+
``broker.publish(...).raise_for_status()``. Carries the ``outcome`` for
|
|
184
|
+
inspection (status, routing_key, underlying error).
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, outcome: object) -> None:
|
|
188
|
+
self.outcome = outcome
|
|
189
|
+
super().__init__(str(outcome))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── Backpressure error ───────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class BackpressureError(Exception):
|
|
196
|
+
"""Raised when publish-side flow control blocks a publish attempt.
|
|
197
|
+
|
|
198
|
+
Only raised when ``BackpressureConfig.on_blocked == "raise"``.
|
|
199
|
+
"""
|