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.
@@ -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()