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,196 @@
1
+ from asyncio import Future
2
+ from random import uniform
3
+ from uuid import UUID, uuid4
4
+
5
+ from pika.adapters.asyncio_connection import AsyncioConnection
6
+ from pika.channel import Channel
7
+ from pika.exceptions import ChannelClosedByClient
8
+ from pika.spec import Basic
9
+
10
+ from ..logging import LoggerProvider
11
+ from ..utils import AsyncEventLoopMixin
12
+
13
+ __all__ = ["BaseChannel"]
14
+
15
+
16
+ class BaseChannel(AsyncEventLoopMixin):
17
+ def __init__(
18
+ self,
19
+ connection: AsyncioConnection,
20
+ reopen_delay: int = 1,
21
+ reopen_delay_jitter: tuple[float, float] = (0.0, 2.0),
22
+ failed_reopen_threshold: int | None = 5,
23
+ ):
24
+ self._connection = connection
25
+ self._reopen_delay = reopen_delay
26
+ self._reopen_delay_jitter = reopen_delay_jitter
27
+ self._failed_reopen_threshold = failed_reopen_threshold
28
+ self._completion_future = self.loop.create_future()
29
+ self._channel = None
30
+ self._channel_id = uuid4()
31
+ self._reopen_failures = 0
32
+ self._reserved = False
33
+ self._can_be_disposed = False
34
+ self._logger = LoggerProvider.default().get_logger(
35
+ "rabbitmq.base_channel"
36
+ )
37
+
38
+ def open(self) -> Future[UUID]:
39
+ if self._channel is not None:
40
+ raise RuntimeError("channel already opened")
41
+
42
+ try:
43
+ _ = self._connection.channel(
44
+ channel_number=None, on_open_callback=self._on_channel_opened
45
+ )
46
+ except Exception as e:
47
+ self._can_be_disposed = True
48
+
49
+ if not self._completion_future.done():
50
+ self._completion_future.set_exception(e)
51
+
52
+ return self._completion_future
53
+
54
+ def reserve(self):
55
+ self._reserved = True
56
+
57
+ def release(self):
58
+ self._reserved = False
59
+
60
+ def _on_channel_opened(self, channel: Channel):
61
+ self._channel = channel
62
+
63
+ try:
64
+ self._hook_on_channel_opened()
65
+ except Exception as e:
66
+ if not self._completion_future.done():
67
+ self._completion_future.set_exception(e)
68
+ else:
69
+ if not self.channel_closed():
70
+ self._channel.close()
71
+
72
+ self._reopen()
73
+
74
+ return
75
+
76
+ self._channel.add_on_cancel_callback(self._on_cancelled)
77
+ self._channel.add_on_close_callback(self._on_channel_closed)
78
+
79
+ self._completion_future.set_result(self._channel_id)
80
+
81
+ def _hook_on_channel_opened(self):
82
+ pass
83
+
84
+ def channel_closed(self) -> bool:
85
+ if self._channel is None:
86
+ raise RuntimeError("underlying channel not set")
87
+
88
+ return self._channel.is_closing or self._channel.is_closed
89
+
90
+ def _on_cancelled(self, method: Basic.Cancel):
91
+ del method
92
+
93
+ if not self._completion_future.done():
94
+ self._completion_future.set_exception(
95
+ RuntimeError("cancellation recieved from rabbitmq server")
96
+ )
97
+
98
+ return
99
+
100
+ try:
101
+ self._hook_on_cancelled()
102
+ except:
103
+ self._logger.exception("error occured on invoking cancel hook")
104
+
105
+ def _hook_on_cancelled(self):
106
+ pass
107
+
108
+ def _on_channel_closed(self, channel: Channel, error: BaseException):
109
+ del channel
110
+
111
+ try:
112
+ self._hook_on_channel_closed(error)
113
+ except:
114
+ self._reopen_failures += 1
115
+
116
+ return
117
+
118
+ if not isinstance(error, ChannelClosedByClient):
119
+ self._reopen()
120
+
121
+ def _hook_on_channel_closed(self, error: BaseException):
122
+ del error
123
+
124
+ def _reopen(self):
125
+ if (
126
+ self._failed_reopen_threshold is not None
127
+ and self._reopen_failures >= self._failed_reopen_threshold
128
+ ):
129
+ self._can_be_disposed = True
130
+
131
+ return
132
+
133
+ self.loop.call_later(
134
+ delay=self._reopen_delay + uniform(*self._reopen_delay_jitter),
135
+ callback=self._on_time_to_reopen,
136
+ )
137
+
138
+ def _on_time_to_reopen(self):
139
+ if self._connection.is_closing or self._connection.is_closed:
140
+ self._can_be_disposed = True
141
+
142
+ return
143
+
144
+ try:
145
+ _ = self._connection.channel(
146
+ channel_number=None, on_open_callback=self._on_channel_opened
147
+ )
148
+ except:
149
+ self._logger.exception(
150
+ "coudn't reopen channel %s", self._channel_id
151
+ )
152
+ self._reopen()
153
+
154
+ def __enter__(self):
155
+ if not self._reserved:
156
+ self.reserve()
157
+
158
+ if self._channel is None:
159
+ raise RuntimeError("underlying channel not opened yet")
160
+
161
+ return self._channel
162
+
163
+ def __exit__(self, *_):
164
+ self.release()
165
+
166
+ @property
167
+ def channel_id(self) -> UUID:
168
+ return self._channel_id
169
+
170
+ @property
171
+ def healthy(self) -> bool:
172
+ return (
173
+ not self._can_be_disposed
174
+ and self._channel is not None
175
+ and not self._channel.is_closing
176
+ and not self._channel.is_closed
177
+ )
178
+
179
+ @property
180
+ def reserved(self) -> bool:
181
+ return self._reserved
182
+
183
+ @property
184
+ def can_be_disposed(self) -> bool:
185
+ return self._can_be_disposed
186
+
187
+ @property
188
+ def connection(self) -> AsyncioConnection:
189
+ return self._connection
190
+
191
+ @property
192
+ def channel(self) -> Channel:
193
+ if self._channel is None:
194
+ raise RuntimeError("underlying channel not opened yet")
195
+
196
+ return self._channel
@@ -0,0 +1,159 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import ValidationError
4
+
5
+ from ..dependencies.miscellaneous import DependsOn
6
+ from ..exceptions import ServiceException, Severity
7
+ from ..logging import LoggerProvider
8
+ from ..logstash._base import BaseLogstashSender
9
+ from ._exceptions import RabbitMQException
10
+ from ._listener import ListenerContext
11
+
12
+ __all__ = [
13
+ "handle_general_mq_exception",
14
+ "handle_microservice_exception",
15
+ "handle_rabbitmq_exception",
16
+ "handle_validation_error",
17
+ ]
18
+
19
+ RABBITMQ_EXCEPTION_HANDLER_LOGGER_NAME = "rabbitmq.exception_handler"
20
+
21
+
22
+ def handle_rabbitmq_exception(
23
+ context: ListenerContext,
24
+ exception: RabbitMQException,
25
+ logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
26
+ logger_provider: Annotated[LoggerProvider, DependsOn(LoggerProvider)],
27
+ ):
28
+ logger = logger_provider.get_logger(RABBITMQ_EXCEPTION_HANDLER_LOGGER_NAME)
29
+
30
+ if not exception.logstash_logging:
31
+ logger.warning("%r", exception)
32
+
33
+ return
34
+
35
+ logstash.warning(
36
+ message=exception.message,
37
+ tags=exception.tags
38
+ or [
39
+ "RabbitMQ",
40
+ "RabbitMQException",
41
+ str(exception.code),
42
+ context.queue,
43
+ context.listener_name or "__default__",
44
+ ],
45
+ extra=exception.extra
46
+ or {
47
+ "serviceType": "RabbitMQ",
48
+ "queue": context.queue,
49
+ "listenerName": context.listener_name,
50
+ "exception": "RabbitMQException",
51
+ },
52
+ )
53
+
54
+
55
+ def handle_validation_error(
56
+ context: ListenerContext,
57
+ exception: ValidationError,
58
+ logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
59
+ ):
60
+ logstash.error(
61
+ message=f"invalid rabbitmq request data at queue `{context.queue}` and listener `{context.listener_name}`",
62
+ tags=[
63
+ "RabbitMQ",
64
+ "ValidationError",
65
+ context.queue,
66
+ context.listener_name or "__default__",
67
+ ],
68
+ extra={
69
+ "serviceType": "RabbitMQ",
70
+ "queue": context.queue,
71
+ "listenerName": context.listener_name,
72
+ "exception": "ValidationError",
73
+ },
74
+ exception=exception,
75
+ )
76
+
77
+
78
+ def handle_microservice_exception(
79
+ context: ListenerContext,
80
+ exception: ServiceException,
81
+ logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
82
+ logger_provider: Annotated[LoggerProvider, DependsOn(LoggerProvider)],
83
+ ):
84
+ logger = logger_provider.get_logger(RABBITMQ_EXCEPTION_HANDLER_LOGGER_NAME)
85
+ tags = [
86
+ "RabbitMQ",
87
+ type(exception).__name__,
88
+ context.queue,
89
+ context.listener_name or "__default__",
90
+ ]
91
+
92
+ if exception.tags:
93
+ exception.tags.extend(tags)
94
+
95
+ extra = {
96
+ "serviceType": "RabbitMQ",
97
+ "queue": context.queue,
98
+ "listenerName": context.listener_name,
99
+ "exception": exception.__class__.__name__,
100
+ }
101
+
102
+ if exception.extra:
103
+ exception.extra.update(extra)
104
+
105
+ exc_info = (
106
+ (type(exception), exception, exception.__traceback__)
107
+ if exception.extract_exc_info
108
+ else None
109
+ )
110
+
111
+ match exception.severity:
112
+ case Severity.HIGH:
113
+ logstash_logger_method = logstash.error
114
+ logger_method = logger.error
115
+ case Severity.MEDIUM:
116
+ logstash_logger_method = logstash.warning
117
+ logger_method = logger.warning
118
+ case _:
119
+ logstash_logger_method = logstash.info
120
+ logger_method = logger.info
121
+
122
+ if exception.logstash_logging:
123
+ logstash_logger_method(
124
+ message=exception.message,
125
+ tags=exception.tags or tags,
126
+ extra=exception.extra or extra,
127
+ exception=exception if exception.extract_exc_info else None,
128
+ )
129
+ else:
130
+ logger_method(
131
+ "\nRabbitMQ `%s` -> `%s`\n%s",
132
+ context.queue,
133
+ context.listener_name,
134
+ exception.message,
135
+ exc_info=exc_info,
136
+ )
137
+
138
+
139
+ def handle_general_mq_exception(
140
+ context: ListenerContext,
141
+ exception: Exception,
142
+ logstash: Annotated[BaseLogstashSender, DependsOn(BaseLogstashSender)],
143
+ ):
144
+ logstash.error(
145
+ message=f"something went wrong while consuming message on queue `{context.queue}` and listener `{context.listener_name}`",
146
+ tags=[
147
+ "RabbitMQ",
148
+ exception.__class__.__name__,
149
+ context.queue,
150
+ context.listener_name or "__default__",
151
+ ],
152
+ extra={
153
+ "serviceType": "RabbitMQ",
154
+ "queue": context.queue,
155
+ "listenerName": context.listener_name,
156
+ "exception": exception.__class__.__name__,
157
+ },
158
+ exception=exception,
159
+ )
@@ -0,0 +1,46 @@
1
+ class RabbitMQException(Exception):
2
+ def __init__(
3
+ self,
4
+ code: int,
5
+ message: str,
6
+ tags: list[str] | None = None,
7
+ extra: dict[str, str] | None = None,
8
+ logstash_logging: bool = True,
9
+ extract_exc_info: bool = True,
10
+ ):
11
+ self._code = code
12
+ self._message = message
13
+ self._tags = tags
14
+ self._extra = extra
15
+ self._logstash_logging = logstash_logging
16
+ self._extract_exc_info = extract_exc_info
17
+
18
+ @property
19
+ def code(self) -> int:
20
+ return self._code
21
+
22
+ @property
23
+ def message(self) -> str:
24
+ return self._message
25
+
26
+ @property
27
+ def tags(self) -> list[str] | None:
28
+ return self._tags
29
+
30
+ @property
31
+ def extra(self) -> dict[str, str] | None:
32
+ return self._extra
33
+
34
+ @property
35
+ def logstash_logging(self) -> bool:
36
+ return self._logstash_logging
37
+
38
+ @property
39
+ def extract_exc_info(self) -> bool:
40
+ return self._extract_exc_info
41
+
42
+ def __str__(self) -> str:
43
+ return f"message `{self.message}`, code `{self.code}`"
44
+
45
+ def __repr__(self) -> str:
46
+ return f"{self.__class__.__name__} ( message: `{self._message}`, code: {self._code} )"