qena-shared-lib 0.1.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.
- qena_shared_lib/__init__.py +27 -0
- qena_shared_lib/application.py +190 -0
- qena_shared_lib/background.py +109 -0
- qena_shared_lib/dependencies/__init__.py +19 -0
- qena_shared_lib/dependencies/http.py +62 -0
- qena_shared_lib/dependencies/miscellaneous.py +35 -0
- qena_shared_lib/exception_handlers.py +165 -0
- qena_shared_lib/exceptions.py +319 -0
- qena_shared_lib/http.py +631 -0
- qena_shared_lib/logging.py +63 -0
- qena_shared_lib/logstash/__init__.py +17 -0
- qena_shared_lib/logstash/_base.py +573 -0
- qena_shared_lib/logstash/_http_sender.py +61 -0
- qena_shared_lib/logstash/_tcp_sender.py +84 -0
- qena_shared_lib/py.typed +0 -0
- qena_shared_lib/rabbitmq/__init__.py +52 -0
- qena_shared_lib/rabbitmq/_base.py +741 -0
- qena_shared_lib/rabbitmq/_channel.py +196 -0
- qena_shared_lib/rabbitmq/_exception_handlers.py +159 -0
- qena_shared_lib/rabbitmq/_exceptions.py +46 -0
- qena_shared_lib/rabbitmq/_listener.py +1292 -0
- qena_shared_lib/rabbitmq/_pool.py +74 -0
- qena_shared_lib/rabbitmq/_publisher.py +73 -0
- qena_shared_lib/rabbitmq/_rpc_client.py +286 -0
- qena_shared_lib/rabbitmq/_utils.py +18 -0
- qena_shared_lib/scheduler.py +402 -0
- qena_shared_lib/security.py +205 -0
- qena_shared_lib/utils.py +28 -0
- qena_shared_lib-0.1.0.dist-info/METADATA +473 -0
- qena_shared_lib-0.1.0.dist-info/RECORD +31 -0
- qena_shared_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,741 @@
|
|
1
|
+
from abc import ABC
|
2
|
+
from asyncio import (
|
3
|
+
Future,
|
4
|
+
Task,
|
5
|
+
gather,
|
6
|
+
iscoroutinefunction,
|
7
|
+
)
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from functools import partial
|
10
|
+
from inspect import Parameter, signature
|
11
|
+
from random import uniform
|
12
|
+
from typing import (
|
13
|
+
Awaitable,
|
14
|
+
Callable,
|
15
|
+
Concatenate,
|
16
|
+
ParamSpec,
|
17
|
+
TypeVar,
|
18
|
+
)
|
19
|
+
|
20
|
+
from pika.adapters.asyncio_connection import AsyncioConnection
|
21
|
+
from pika.connection import Parameters, URLParameters
|
22
|
+
from pika.exceptions import ChannelClosedByClient, ConnectionClosedByClient
|
23
|
+
from pika.frame import Method
|
24
|
+
from prometheus_client import Counter
|
25
|
+
from prometheus_client import Enum as PrometheusEnum
|
26
|
+
from punq import Container, Scope
|
27
|
+
from pydantic import ValidationError
|
28
|
+
|
29
|
+
from ..dependencies.miscellaneous import validate_annotation
|
30
|
+
from ..exceptions import ServiceException
|
31
|
+
from ..logging import LoggerProvider
|
32
|
+
from ..logstash import BaseLogstashSender
|
33
|
+
from ..utils import AsyncEventLoopMixin
|
34
|
+
from ._exception_handlers import (
|
35
|
+
handle_general_mq_exception,
|
36
|
+
handle_microservice_exception,
|
37
|
+
handle_rabbitmq_exception,
|
38
|
+
handle_validation_error,
|
39
|
+
)
|
40
|
+
from ._exceptions import RabbitMQException
|
41
|
+
from ._listener import (
|
42
|
+
LISTENER_ATTRIBUTE,
|
43
|
+
Listener,
|
44
|
+
ListenerBase,
|
45
|
+
ListenerContext,
|
46
|
+
RetryPolicy,
|
47
|
+
)
|
48
|
+
from ._pool import ChannelPool
|
49
|
+
from ._publisher import Publisher
|
50
|
+
from ._rpc_client import RpcClient
|
51
|
+
|
52
|
+
__all__ = [
|
53
|
+
"AbstractRabbitMQService",
|
54
|
+
"RabbitMqManager",
|
55
|
+
]
|
56
|
+
|
57
|
+
E = TypeVar("E")
|
58
|
+
P = ParamSpec("P")
|
59
|
+
ExceptionHandler = (
|
60
|
+
Callable[Concatenate[ListenerContext, E, P], None]
|
61
|
+
| Callable[Concatenate[ListenerContext, E, P], Awaitable]
|
62
|
+
)
|
63
|
+
|
64
|
+
|
65
|
+
class AbstractRabbitMQService(ABC):
|
66
|
+
def initialize(
|
67
|
+
self, connection: AsyncioConnection, channel_pool: ChannelPool
|
68
|
+
) -> Future:
|
69
|
+
raise NotImplementedError()
|
70
|
+
|
71
|
+
|
72
|
+
@dataclass
|
73
|
+
class ExceptionHandlerContainer:
|
74
|
+
handler: ExceptionHandler
|
75
|
+
dependencies: dict[str, type]
|
76
|
+
|
77
|
+
|
78
|
+
class RabbitMqManager(AsyncEventLoopMixin):
|
79
|
+
RABBITMQ_CONNECTION_STATE = PrometheusEnum(
|
80
|
+
name="rabbitmq_connection_state",
|
81
|
+
documentation="Babbitmq connection state",
|
82
|
+
states=["connected", "reconnecting", "disconnected"],
|
83
|
+
)
|
84
|
+
RABBITMQ_PUBLISHER_BLOCKED_STATE = PrometheusEnum(
|
85
|
+
name="rabbitmq_publisher_blocked_state",
|
86
|
+
documentation="Rabbitmq publisher blocked state",
|
87
|
+
states=["blocked", "unblocked"],
|
88
|
+
)
|
89
|
+
HANDLED_EXCEPTIONS = Counter(
|
90
|
+
name="handled_exceptions",
|
91
|
+
documentation="Handled exceptions",
|
92
|
+
labelnames=["queue", "listener_name", "exception"],
|
93
|
+
)
|
94
|
+
|
95
|
+
def __init__(
|
96
|
+
self,
|
97
|
+
listeners: list[Listener | type[ListenerBase]],
|
98
|
+
logstash: BaseLogstashSender,
|
99
|
+
parameters: Parameters | str | None = None,
|
100
|
+
reconnect_delay: float = 5.0,
|
101
|
+
reconnect_delay_jitter: tuple[float, float] = (1.0, 5.0),
|
102
|
+
listener_global_retry_policy: RetryPolicy | None = None,
|
103
|
+
container: Container | None = None,
|
104
|
+
):
|
105
|
+
for index, listener in enumerate(listeners):
|
106
|
+
if not isinstance(listener, Listener) and (
|
107
|
+
not isinstance(listener, type)
|
108
|
+
or not issubclass(listener, ListenerBase)
|
109
|
+
):
|
110
|
+
raise TypeError(
|
111
|
+
f"listener {index} is {type(listener)}, expected instance of type or subclass of `Listener` or `type[ListenerBase]`"
|
112
|
+
)
|
113
|
+
|
114
|
+
self._listener_classes = [
|
115
|
+
listener_class
|
116
|
+
for listener_class in listeners
|
117
|
+
if isinstance(listener_class, type)
|
118
|
+
and issubclass(listener_class, ListenerBase)
|
119
|
+
]
|
120
|
+
self._listeners = [
|
121
|
+
listener for listener in listeners if isinstance(listener, Listener)
|
122
|
+
]
|
123
|
+
|
124
|
+
if isinstance(parameters, str):
|
125
|
+
self._parameters = URLParameters(parameters)
|
126
|
+
else:
|
127
|
+
self._parameters = parameters
|
128
|
+
|
129
|
+
self._reconnect_delay = reconnect_delay
|
130
|
+
self._reconnect_delay_jitter = reconnect_delay_jitter
|
131
|
+
self._container = container or Container()
|
132
|
+
self._listener_global_retry_policy = listener_global_retry_policy
|
133
|
+
self._connection = None
|
134
|
+
self._connected = False
|
135
|
+
self._disconnected = False
|
136
|
+
self._connection_blocked = False
|
137
|
+
self._services = []
|
138
|
+
self._exception_handlers: dict[
|
139
|
+
type[Exception], ExceptionHandlerContainer
|
140
|
+
] = {}
|
141
|
+
|
142
|
+
self._register_listener_classes()
|
143
|
+
|
144
|
+
self.exception_handler(RabbitMQException)(handle_rabbitmq_exception)
|
145
|
+
self.exception_handler(ValidationError)(handle_validation_error)
|
146
|
+
self.exception_handler(ServiceException)(handle_microservice_exception)
|
147
|
+
self.exception_handler(Exception)(handle_general_mq_exception)
|
148
|
+
|
149
|
+
self._channel_pool = ChannelPool()
|
150
|
+
self._logstash = logstash
|
151
|
+
self._logger = LoggerProvider.default().get_logger("rabbitmq")
|
152
|
+
|
153
|
+
def _register_listener_classes(self):
|
154
|
+
for listener_class in self._listener_classes:
|
155
|
+
inner_listener = getattr(listener_class, LISTENER_ATTRIBUTE, None)
|
156
|
+
|
157
|
+
if inner_listener is None:
|
158
|
+
raise AttributeError(
|
159
|
+
"listener is possibly not with `Consumer` or `RpcWorker`"
|
160
|
+
)
|
161
|
+
|
162
|
+
if not isinstance(inner_listener, Listener):
|
163
|
+
raise TypeError(
|
164
|
+
f"listener class {type(listener_class)} is not a type `Listener`, posibilly not decorated with `Consumer` or `RpcWorker`"
|
165
|
+
)
|
166
|
+
|
167
|
+
self._container.register(
|
168
|
+
service=ListenerBase,
|
169
|
+
factory=listener_class,
|
170
|
+
scope=Scope.singleton,
|
171
|
+
)
|
172
|
+
|
173
|
+
@property
|
174
|
+
def container(self) -> Container:
|
175
|
+
return self._container
|
176
|
+
|
177
|
+
def exception_handler(
|
178
|
+
self, exception: type[Exception]
|
179
|
+
) -> Callable[[ExceptionHandler], ExceptionHandler]:
|
180
|
+
if not issubclass(exception, Exception):
|
181
|
+
raise TypeError(
|
182
|
+
f"exception should be of type `Exception`, got {exception}"
|
183
|
+
)
|
184
|
+
|
185
|
+
def wrapper(
|
186
|
+
handler: ExceptionHandler,
|
187
|
+
):
|
188
|
+
if not callable(handler):
|
189
|
+
raise TypeError(f"handler not a callable, got {type(handler)}")
|
190
|
+
|
191
|
+
dependencies = {}
|
192
|
+
|
193
|
+
for parameter_position, (parameter_name, parameter) in enumerate(
|
194
|
+
signature(handler).parameters.items()
|
195
|
+
):
|
196
|
+
if 0 <= parameter_position < 2:
|
197
|
+
is_valid, expected_type = (
|
198
|
+
self._is_valid_exception_handler_parameter(
|
199
|
+
position=parameter_position,
|
200
|
+
annotation=parameter.annotation,
|
201
|
+
)
|
202
|
+
)
|
203
|
+
|
204
|
+
if not is_valid:
|
205
|
+
raise TypeError(
|
206
|
+
f"parameter `{parameter_name}` at `{parameter_position + 1}` is not annotated with type or subclass of `{expected_type}`, got {parameter.annotation}"
|
207
|
+
)
|
208
|
+
|
209
|
+
continue
|
210
|
+
|
211
|
+
dependency = validate_annotation(parameter)
|
212
|
+
|
213
|
+
if dependency is None:
|
214
|
+
raise ValueError(
|
215
|
+
f"handlers cannot contain parameters other than `Annotated[type, DependsOn(type)]`, got `{parameter_name}: {dependency}`"
|
216
|
+
)
|
217
|
+
|
218
|
+
dependencies[parameter_name] = dependency
|
219
|
+
|
220
|
+
self._exception_handlers[exception] = ExceptionHandlerContainer(
|
221
|
+
handler=handler, dependencies=dependencies
|
222
|
+
)
|
223
|
+
|
224
|
+
return handler
|
225
|
+
|
226
|
+
return wrapper
|
227
|
+
|
228
|
+
def _is_valid_exception_handler_parameter(
|
229
|
+
self, position: int, annotation: type
|
230
|
+
) -> tuple[bool, type | None]:
|
231
|
+
if annotation is Parameter.empty:
|
232
|
+
return True, None
|
233
|
+
|
234
|
+
if position == 0 and annotation is not ListenerContext:
|
235
|
+
return False, ListenerContext
|
236
|
+
|
237
|
+
if position == 1 and not issubclass(annotation, Exception):
|
238
|
+
return False, Exception
|
239
|
+
|
240
|
+
return True, None
|
241
|
+
|
242
|
+
def include_service(
|
243
|
+
self,
|
244
|
+
rabbit_mq_service: AbstractRabbitMQService
|
245
|
+
| type[AbstractRabbitMQService],
|
246
|
+
):
|
247
|
+
if not isinstance(rabbit_mq_service, AbstractRabbitMQService) and (
|
248
|
+
not isinstance(rabbit_mq_service, type)
|
249
|
+
or not issubclass(rabbit_mq_service, AbstractRabbitMQService)
|
250
|
+
):
|
251
|
+
raise TypeError(
|
252
|
+
f"rabbitmq service is not type of `AbstractRabbitMQService`, got `{type(rabbit_mq_service)}`"
|
253
|
+
)
|
254
|
+
|
255
|
+
if isinstance(rabbit_mq_service, AbstractRabbitMQService):
|
256
|
+
self._services.append(rabbit_mq_service)
|
257
|
+
else:
|
258
|
+
self._container.register(
|
259
|
+
service=AbstractRabbitMQService,
|
260
|
+
factory=rabbit_mq_service,
|
261
|
+
scope=Scope.singleton,
|
262
|
+
)
|
263
|
+
|
264
|
+
def connect(self) -> Future:
|
265
|
+
if not self._connected:
|
266
|
+
self._resolve_listener_classes()
|
267
|
+
self._resolve_service_classes()
|
268
|
+
|
269
|
+
if self._is_connection_healthy():
|
270
|
+
raise RuntimeError("rabbitmq already connected and healthy")
|
271
|
+
|
272
|
+
self._connected_future = self.loop.create_future()
|
273
|
+
_ = AsyncioConnection(
|
274
|
+
parameters=self._parameters,
|
275
|
+
on_open_callback=self._on_connection_opened,
|
276
|
+
on_open_error_callback=self._on_connection_open_error,
|
277
|
+
on_close_callback=self._on_connection_closed,
|
278
|
+
custom_ioloop=self.loop,
|
279
|
+
)
|
280
|
+
|
281
|
+
return self._connected_future
|
282
|
+
|
283
|
+
def _resolve_listener_classes(self):
|
284
|
+
self._listeners.extend(
|
285
|
+
listener.register_listener_methods()
|
286
|
+
for listener in self._container.resolve_all(ListenerBase)
|
287
|
+
)
|
288
|
+
|
289
|
+
def _resolve_service_classes(self):
|
290
|
+
self._services.extend(
|
291
|
+
self._container.resolve_all(AbstractRabbitMQService)
|
292
|
+
)
|
293
|
+
|
294
|
+
@property
|
295
|
+
def connection(self) -> AsyncioConnection:
|
296
|
+
if not self._is_connection_healthy():
|
297
|
+
raise RuntimeError("connection not ready yet")
|
298
|
+
|
299
|
+
assert self._connection is not None
|
300
|
+
|
301
|
+
return self._connection
|
302
|
+
|
303
|
+
def publisher(
|
304
|
+
self,
|
305
|
+
routing_key: str,
|
306
|
+
exchange: str | None = None,
|
307
|
+
target: str | None = None,
|
308
|
+
headers: dict[str, str] | None = None,
|
309
|
+
) -> Publisher:
|
310
|
+
if not self._is_connection_healthy():
|
311
|
+
raise RuntimeError("rabbitmq connection is not healthy")
|
312
|
+
|
313
|
+
return Publisher(
|
314
|
+
routing_key=routing_key,
|
315
|
+
channel_pool=self._channel_pool,
|
316
|
+
blocked_connection_check_callback=self._is_connection_blocked,
|
317
|
+
exchange=exchange,
|
318
|
+
target=target,
|
319
|
+
headers=headers,
|
320
|
+
)
|
321
|
+
|
322
|
+
def rpc_client(
|
323
|
+
self,
|
324
|
+
routing_key: str,
|
325
|
+
exchange: str | None = None,
|
326
|
+
procedure: str | None = None,
|
327
|
+
headers: dict[str, str] | None = None,
|
328
|
+
return_type: type | None = None,
|
329
|
+
timeout: float = 0,
|
330
|
+
) -> RpcClient:
|
331
|
+
if timeout < 0:
|
332
|
+
raise ValueError(f"timeout cannot below 0, got {timeout} seconds")
|
333
|
+
|
334
|
+
if timeout == 0:
|
335
|
+
self._logger.warning(
|
336
|
+
"rpc call with 0 seconds timeout may never return back"
|
337
|
+
)
|
338
|
+
|
339
|
+
if not self._is_connection_healthy():
|
340
|
+
raise RuntimeError("rabbitmq connection is not healthy")
|
341
|
+
|
342
|
+
return RpcClient(
|
343
|
+
routing_key=routing_key,
|
344
|
+
channel_pool=self._channel_pool,
|
345
|
+
blocked_connection_check_callback=self._is_connection_blocked,
|
346
|
+
exchange=exchange,
|
347
|
+
procedure=procedure,
|
348
|
+
headers=headers,
|
349
|
+
return_type=return_type,
|
350
|
+
timeout=timeout,
|
351
|
+
)
|
352
|
+
|
353
|
+
def _is_connection_healthy(self) -> bool:
|
354
|
+
return (
|
355
|
+
self._connected
|
356
|
+
and self._connection is not None
|
357
|
+
and not self._connection.is_closing
|
358
|
+
and not self._connection.is_closed
|
359
|
+
)
|
360
|
+
|
361
|
+
def disconnect(self):
|
362
|
+
if self._disconnected:
|
363
|
+
raise RuntimeError("already disconnected from rabbitmq")
|
364
|
+
|
365
|
+
self._disconnected = True
|
366
|
+
|
367
|
+
if self._connection is None:
|
368
|
+
raise RuntimeError("connection not ready yet")
|
369
|
+
|
370
|
+
if self._connection.is_closing or self._connection.is_closed:
|
371
|
+
self._logger.info("already disconnected from rabbitmq")
|
372
|
+
else:
|
373
|
+
self._connection.close()
|
374
|
+
self._logger.info("disconnected from rabbitmq")
|
375
|
+
|
376
|
+
self.RABBITMQ_CONNECTION_STATE.state("disconnected")
|
377
|
+
|
378
|
+
def _on_connection_opened(self, connection: AsyncioConnection):
|
379
|
+
self._connection = connection
|
380
|
+
self._connection_blocked = False
|
381
|
+
|
382
|
+
self._connection.add_on_connection_blocked_callback(
|
383
|
+
self._on_connection_blocked
|
384
|
+
)
|
385
|
+
self._connection.add_on_connection_unblocked_callback(
|
386
|
+
self._on_connection_unblocked
|
387
|
+
)
|
388
|
+
|
389
|
+
if self._connected:
|
390
|
+
self.loop.create_task(self._channel_pool.dain()).add_done_callback(
|
391
|
+
self._channel_pool_drained
|
392
|
+
)
|
393
|
+
|
394
|
+
return
|
395
|
+
|
396
|
+
self.loop.create_task(
|
397
|
+
self._channel_pool.fill(self._connection)
|
398
|
+
).add_done_callback(self._channel_pool_filled)
|
399
|
+
|
400
|
+
def _channel_pool_drained(self, task: Task):
|
401
|
+
if task.cancelled():
|
402
|
+
if not self._connected and not self._connected_future.done():
|
403
|
+
_ = self._connected_future.cancel(None)
|
404
|
+
|
405
|
+
return
|
406
|
+
|
407
|
+
exception = task.exception()
|
408
|
+
|
409
|
+
if exception is not None:
|
410
|
+
if not self._disconnected and not isinstance(
|
411
|
+
exception, (ConnectionClosedByClient, ChannelClosedByClient)
|
412
|
+
):
|
413
|
+
self._logstash.error(
|
414
|
+
message="couldn't drain the channel pool",
|
415
|
+
exception=exception,
|
416
|
+
)
|
417
|
+
self._reconnect()
|
418
|
+
|
419
|
+
return
|
420
|
+
|
421
|
+
if self._connection is None:
|
422
|
+
raise RuntimeError("connection not ready yet")
|
423
|
+
|
424
|
+
self.loop.create_task(
|
425
|
+
self._channel_pool.fill(self._connection)
|
426
|
+
).add_done_callback(self._channel_pool_filled)
|
427
|
+
|
428
|
+
def _on_connection_blocked(
|
429
|
+
self, connection: AsyncioConnection, method: Method
|
430
|
+
):
|
431
|
+
del connection, method
|
432
|
+
|
433
|
+
self._connection_blocked = True
|
434
|
+
|
435
|
+
self._logstash.warning(
|
436
|
+
"connection is blocked by broker, will not accept published messages"
|
437
|
+
)
|
438
|
+
self.RABBITMQ_PUBLISHER_BLOCKED_STATE.state("blocked")
|
439
|
+
|
440
|
+
def _on_connection_unblocked(
|
441
|
+
self, connection: AsyncioConnection, method: Method
|
442
|
+
):
|
443
|
+
del connection, method
|
444
|
+
|
445
|
+
self._connection_blocked = False
|
446
|
+
|
447
|
+
self._logstash.info("broker resumed accepting published messages")
|
448
|
+
self.RABBITMQ_PUBLISHER_BLOCKED_STATE.state("unblocked")
|
449
|
+
|
450
|
+
def _is_connection_blocked(self) -> bool:
|
451
|
+
return self._connection_blocked
|
452
|
+
|
453
|
+
def _channel_pool_filled(self, task: Task):
|
454
|
+
if task.cancelled():
|
455
|
+
if not self._connected and not self._connected_future.done():
|
456
|
+
_ = self._connected_future.cancel(None)
|
457
|
+
|
458
|
+
return
|
459
|
+
|
460
|
+
exception = task.exception()
|
461
|
+
|
462
|
+
if exception is not None:
|
463
|
+
if not self._connected and not self._connected_future.done():
|
464
|
+
self._connected_future.set_exception(exception)
|
465
|
+
elif (
|
466
|
+
self._connected
|
467
|
+
and not self._disconnected
|
468
|
+
and not isinstance(
|
469
|
+
exception, (ConnectionClosedByClient, ChannelClosedByClient)
|
470
|
+
)
|
471
|
+
):
|
472
|
+
self._logstash.error(
|
473
|
+
message="couldn't fill the channel pool",
|
474
|
+
exception=exception,
|
475
|
+
)
|
476
|
+
self._reconnect()
|
477
|
+
|
478
|
+
return
|
479
|
+
|
480
|
+
if self._connection is None:
|
481
|
+
raise RuntimeError("connection not ready yet")
|
482
|
+
|
483
|
+
channel_count = len(self._channel_pool)
|
484
|
+
|
485
|
+
self._logger.debug(
|
486
|
+
"global channel pool filled with `%d` %s",
|
487
|
+
channel_count,
|
488
|
+
"channel" if 0 <= channel_count <= 1 else "channels",
|
489
|
+
)
|
490
|
+
gather(
|
491
|
+
*self._configure_listeners(), *self._initialize_services()
|
492
|
+
).add_done_callback(self._listener_and_service_config_and_init_done)
|
493
|
+
|
494
|
+
def _configure_listeners(self) -> list[Awaitable]:
|
495
|
+
try:
|
496
|
+
assert self._connection is not None
|
497
|
+
|
498
|
+
return [
|
499
|
+
listener.configure(
|
500
|
+
connection=self._connection,
|
501
|
+
channel_pool=self._channel_pool,
|
502
|
+
on_exception_callback=self._invoke_exception_handler,
|
503
|
+
container=self._container,
|
504
|
+
logstash=self._logstash,
|
505
|
+
global_retry_policy=self._listener_global_retry_policy,
|
506
|
+
)
|
507
|
+
for listener in self._listeners
|
508
|
+
]
|
509
|
+
except Exception as e:
|
510
|
+
listener_configuration_error_future = self.loop.create_future()
|
511
|
+
|
512
|
+
listener_configuration_error_future.set_exception(e)
|
513
|
+
|
514
|
+
return [listener_configuration_error_future]
|
515
|
+
|
516
|
+
def _initialize_services(self) -> list[Awaitable]:
|
517
|
+
try:
|
518
|
+
return [
|
519
|
+
service.initialize(self._connection, self._channel_pool)
|
520
|
+
for service in self._services
|
521
|
+
]
|
522
|
+
except Exception as e:
|
523
|
+
service_initialization_error_future = self.loop.create_future()
|
524
|
+
|
525
|
+
service_initialization_error_future.set_exception(e)
|
526
|
+
|
527
|
+
return [service_initialization_error_future]
|
528
|
+
|
529
|
+
def _listener_and_service_config_and_init_done(self, future: Future):
|
530
|
+
if future.cancelled():
|
531
|
+
if not self._connected and not self._connected_future.done():
|
532
|
+
_ = self._connected_future.cancel(None)
|
533
|
+
|
534
|
+
return
|
535
|
+
|
536
|
+
exception = future.exception()
|
537
|
+
|
538
|
+
if exception is not None:
|
539
|
+
if not self._connected and not self._connected_future.done():
|
540
|
+
self._connected_future.set_exception(exception)
|
541
|
+
elif (
|
542
|
+
self._connected
|
543
|
+
and not self._disconnected
|
544
|
+
and not isinstance(
|
545
|
+
exception, (ConnectionClosedByClient, ChannelClosedByClient)
|
546
|
+
)
|
547
|
+
):
|
548
|
+
self._logstash.error(
|
549
|
+
message="couldn't configure and initialize all listeners and services",
|
550
|
+
exception=exception,
|
551
|
+
)
|
552
|
+
self._reconnect()
|
553
|
+
|
554
|
+
return
|
555
|
+
|
556
|
+
if not self._connected:
|
557
|
+
self._connected = True
|
558
|
+
|
559
|
+
if self._connection is None:
|
560
|
+
raise RuntimeError("connection not ready yet")
|
561
|
+
|
562
|
+
params = self._connection.params
|
563
|
+
listener_count = len(self._listeners)
|
564
|
+
service_count = len(self._services)
|
565
|
+
|
566
|
+
self._logger.info(
|
567
|
+
"connect to rabbitmq, `%s:%s%s` with `%d` %s and `%d` %s.",
|
568
|
+
params.host,
|
569
|
+
params.port,
|
570
|
+
params.virtual_host,
|
571
|
+
listener_count,
|
572
|
+
"listeners" if listener_count > 1 else "listener",
|
573
|
+
service_count,
|
574
|
+
"services" if service_count > 1 else "service",
|
575
|
+
)
|
576
|
+
|
577
|
+
if not self._connected_future.done():
|
578
|
+
self._connected_future.set_result(None)
|
579
|
+
|
580
|
+
self.RABBITMQ_CONNECTION_STATE.state("connected")
|
581
|
+
|
582
|
+
def _on_connection_open_error(
|
583
|
+
self, connection: AsyncioConnection, exception: BaseException
|
584
|
+
):
|
585
|
+
del connection
|
586
|
+
|
587
|
+
if not self._connected and not self._connected_future.done():
|
588
|
+
self._connected_future.set_exception(exception)
|
589
|
+
|
590
|
+
return
|
591
|
+
|
592
|
+
if self._connected and not self._disconnected:
|
593
|
+
self._logstash.error(
|
594
|
+
message="error while opening connection to rabbitmq",
|
595
|
+
exception=exception,
|
596
|
+
)
|
597
|
+
self._reconnect()
|
598
|
+
|
599
|
+
def _invoke_exception_handler(
|
600
|
+
self, context: ListenerContext, exception: BaseException
|
601
|
+
) -> bool:
|
602
|
+
exception_handler = None
|
603
|
+
|
604
|
+
for exception_type in type(exception).mro():
|
605
|
+
exception_handler = self._exception_handlers.get(exception_type)
|
606
|
+
|
607
|
+
if exception_handler is not None:
|
608
|
+
break
|
609
|
+
|
610
|
+
if exception_handler is None:
|
611
|
+
return False
|
612
|
+
|
613
|
+
dependencies = {}
|
614
|
+
|
615
|
+
for (
|
616
|
+
parameter_name,
|
617
|
+
dependency,
|
618
|
+
) in exception_handler.dependencies.items():
|
619
|
+
dependencies[parameter_name] = self._container.resolve(dependency)
|
620
|
+
|
621
|
+
if iscoroutinefunction(exception_handler.handler):
|
622
|
+
self.loop.create_task(
|
623
|
+
exception_handler.handler(context, exception, **dependencies)
|
624
|
+
).add_done_callback(
|
625
|
+
partial(self._on_exception_handler_done, context)
|
626
|
+
)
|
627
|
+
else:
|
628
|
+
self.loop.run_in_executor(
|
629
|
+
executor=None,
|
630
|
+
func=partial(
|
631
|
+
exception_handler.handler,
|
632
|
+
context,
|
633
|
+
exception,
|
634
|
+
**dependencies,
|
635
|
+
),
|
636
|
+
).add_done_callback(
|
637
|
+
partial(self._on_exception_handler_done, context)
|
638
|
+
)
|
639
|
+
|
640
|
+
self.HANDLED_EXCEPTIONS.labels(
|
641
|
+
queue=context.queue,
|
642
|
+
listener_name=context.listener_name,
|
643
|
+
exception=exception.__class__.__name__,
|
644
|
+
).inc()
|
645
|
+
|
646
|
+
return True
|
647
|
+
|
648
|
+
def _on_exception_handler_done(
|
649
|
+
self, context: ListenerContext, task_or_future: Task | Future
|
650
|
+
):
|
651
|
+
if task_or_future.cancelled():
|
652
|
+
return
|
653
|
+
|
654
|
+
exception = task_or_future.exception()
|
655
|
+
|
656
|
+
if exception is not None:
|
657
|
+
self._logstash.error(
|
658
|
+
message="error occured in listener exception handler",
|
659
|
+
exception=exception,
|
660
|
+
)
|
661
|
+
|
662
|
+
context.dispose()
|
663
|
+
|
664
|
+
def _on_connection_closed(
|
665
|
+
self, connection: AsyncioConnection, exception: BaseException
|
666
|
+
):
|
667
|
+
del connection
|
668
|
+
|
669
|
+
if not self._connected and not self._connected_future.done():
|
670
|
+
self._connected_future.set_exception(exception)
|
671
|
+
|
672
|
+
return
|
673
|
+
|
674
|
+
if (
|
675
|
+
self._connected
|
676
|
+
and not self._disconnected
|
677
|
+
and not isinstance(
|
678
|
+
exception, (ConnectionClosedByClient, ChannelClosedByClient)
|
679
|
+
)
|
680
|
+
):
|
681
|
+
self._logstash.error(
|
682
|
+
message="connection to rabbitmq closed unexpectedly, attempting to reconnect",
|
683
|
+
exception=exception,
|
684
|
+
)
|
685
|
+
self._reconnect()
|
686
|
+
|
687
|
+
def _reconnect(self):
|
688
|
+
self.RABBITMQ_CONNECTION_STATE.state("reconnecting")
|
689
|
+
self.loop.call_later(
|
690
|
+
delay=self._reconnect_delay
|
691
|
+
+ uniform(*self._reconnect_delay_jitter),
|
692
|
+
callback=self._on_time_to_reconnect,
|
693
|
+
)
|
694
|
+
|
695
|
+
def _on_time_to_reconnect(self):
|
696
|
+
try:
|
697
|
+
connected_future = self.connect()
|
698
|
+
except Exception as e:
|
699
|
+
if (
|
700
|
+
self._connected
|
701
|
+
and not self._disconnected
|
702
|
+
and not isinstance(
|
703
|
+
e, (ConnectionClosedByClient, ChannelClosedByClient)
|
704
|
+
)
|
705
|
+
):
|
706
|
+
if not self._connected_future.done():
|
707
|
+
self._connected_future.set_result(None)
|
708
|
+
|
709
|
+
self._logstash.exception(
|
710
|
+
"couldn't reconnect to rabbitmq, attempting to reconnect"
|
711
|
+
)
|
712
|
+
self._reconnect()
|
713
|
+
|
714
|
+
return
|
715
|
+
|
716
|
+
if connected_future.done():
|
717
|
+
return
|
718
|
+
|
719
|
+
connected_future.add_done_callback(self._on_reconnect_done)
|
720
|
+
|
721
|
+
def _on_reconnect_done(self, future: Future):
|
722
|
+
if future.cancelled():
|
723
|
+
return
|
724
|
+
|
725
|
+
exception = future.exception()
|
726
|
+
|
727
|
+
if exception is None:
|
728
|
+
return
|
729
|
+
|
730
|
+
if (
|
731
|
+
self._connected
|
732
|
+
and not self._disconnected
|
733
|
+
and not isinstance(
|
734
|
+
exception, (ConnectionClosedByClient, ChannelClosedByClient)
|
735
|
+
)
|
736
|
+
):
|
737
|
+
self._logstash.error(
|
738
|
+
message="couldn't reconnect to rabbitmq, attempting to reconnect",
|
739
|
+
exception=exception,
|
740
|
+
)
|
741
|
+
self._reconnect()
|