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,1292 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from asyncio import AbstractEventLoop, Future, Lock, Task, iscoroutinefunction
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from functools import partial
|
5
|
+
from inspect import Parameter, signature
|
6
|
+
from random import uniform
|
7
|
+
from time import time
|
8
|
+
from types import MappingProxyType
|
9
|
+
from typing import Any, Callable, Collection, TypeVar
|
10
|
+
|
11
|
+
from pika import BasicProperties
|
12
|
+
from pika.adapters.asyncio_connection import AsyncioConnection
|
13
|
+
from pika.channel import Channel
|
14
|
+
from pika.frame import Method
|
15
|
+
from pika.spec import Basic
|
16
|
+
from prometheus_client import Counter, Summary
|
17
|
+
from punq import Container
|
18
|
+
from pydantic import ValidationError
|
19
|
+
from pydantic_core import from_json, to_json
|
20
|
+
|
21
|
+
from ..dependencies.miscellaneous import validate_annotation
|
22
|
+
from ..logging import LoggerProvider
|
23
|
+
from ..logstash import BaseLogstashSender
|
24
|
+
from ..utils import AsyncEventLoopMixin
|
25
|
+
from ._channel import BaseChannel
|
26
|
+
from ._exceptions import RabbitMQException
|
27
|
+
from ._pool import ChannelPool
|
28
|
+
from ._utils import TypeAdapterCache
|
29
|
+
|
30
|
+
__all__ = [
|
31
|
+
"BackoffRetryDelay",
|
32
|
+
"consume",
|
33
|
+
"CONSUMER_ATTRIBUTE",
|
34
|
+
"Consumer",
|
35
|
+
"execute",
|
36
|
+
"FixedRetryDelay",
|
37
|
+
"FlowControl",
|
38
|
+
"LISTENER_ATTRIBUTE",
|
39
|
+
"Listener",
|
40
|
+
"ListenerBase",
|
41
|
+
"ListenerContext",
|
42
|
+
"RetryDelayJitter",
|
43
|
+
"RetryDelayStrategy",
|
44
|
+
"RetryPolicy",
|
45
|
+
"RPC_WORKER_ATTRIBUTE",
|
46
|
+
"RpcReply",
|
47
|
+
"RpcWorker",
|
48
|
+
]
|
49
|
+
|
50
|
+
|
51
|
+
L = TypeVar("L")
|
52
|
+
DEFAULT_EXCHANGE = ""
|
53
|
+
LISTENER_ATTRIBUTE = "__rabbitmq_listener__"
|
54
|
+
CONSUMER_ATTRIBUTE = "__rabbitmq_consumer__"
|
55
|
+
RPC_WORKER_ATTRIBUTE = "__rabbitmq_rpc_worker__"
|
56
|
+
|
57
|
+
|
58
|
+
class FlowControl:
|
59
|
+
def __init__(self, channel: Channel, loop: AbstractEventLoop):
|
60
|
+
self._channel = channel
|
61
|
+
self._loop = loop
|
62
|
+
self._lock = Lock()
|
63
|
+
self._flow_control_future = None
|
64
|
+
|
65
|
+
async def request(self, prefetch_count: int):
|
66
|
+
async with self._lock:
|
67
|
+
self._flow_control_future = self._loop.create_future()
|
68
|
+
|
69
|
+
self._channel.basic_qos(
|
70
|
+
prefetch_count=prefetch_count,
|
71
|
+
callback=self._on_prefetch_count_set,
|
72
|
+
)
|
73
|
+
|
74
|
+
await self._flow_control_future
|
75
|
+
|
76
|
+
def _on_prefetch_count_set(self, method: Method):
|
77
|
+
del method
|
78
|
+
|
79
|
+
if self._flow_control_future is None:
|
80
|
+
raise RuntimeError("flow control future not set")
|
81
|
+
|
82
|
+
self._flow_control_future.set_result(None)
|
83
|
+
|
84
|
+
|
85
|
+
class RpcReply:
|
86
|
+
def __init__(
|
87
|
+
self,
|
88
|
+
channel_pool: ChannelPool,
|
89
|
+
reply_to: str,
|
90
|
+
correlation_id: str | None = None,
|
91
|
+
):
|
92
|
+
self._channel_pool = channel_pool
|
93
|
+
self._reply_to = reply_to
|
94
|
+
self._correlation_id = correlation_id
|
95
|
+
self._replied = False
|
96
|
+
|
97
|
+
async def reply(self, message: Any):
|
98
|
+
base_channel = await self._channel_pool.get()
|
99
|
+
|
100
|
+
reply_properties = BasicProperties(content_type="application/json")
|
101
|
+
|
102
|
+
if self._correlation_id is not None:
|
103
|
+
reply_properties.correlation_id = self._correlation_id
|
104
|
+
|
105
|
+
try:
|
106
|
+
with base_channel as channel:
|
107
|
+
channel.basic_publish(
|
108
|
+
exchange=DEFAULT_EXCHANGE,
|
109
|
+
routing_key=self._reply_to,
|
110
|
+
properties=reply_properties,
|
111
|
+
body=to_json(message),
|
112
|
+
)
|
113
|
+
except:
|
114
|
+
return
|
115
|
+
|
116
|
+
self._replied = True
|
117
|
+
|
118
|
+
@property
|
119
|
+
def replied(self) -> bool:
|
120
|
+
return self._replied
|
121
|
+
|
122
|
+
|
123
|
+
@dataclass
|
124
|
+
class ListenerContext:
|
125
|
+
queue: str
|
126
|
+
listener_name: str
|
127
|
+
body: bytes
|
128
|
+
flow_control: FlowControl
|
129
|
+
rpc_reply: RpcReply | None = None
|
130
|
+
context_dispose_callback: Callable | None = None
|
131
|
+
|
132
|
+
def dispose(self):
|
133
|
+
if self.context_dispose_callback is not None:
|
134
|
+
self.context_dispose_callback(self)
|
135
|
+
|
136
|
+
|
137
|
+
class RetryDelayStrategy(ABC):
|
138
|
+
@abstractmethod
|
139
|
+
def delay(self, times_rejected: int) -> float:
|
140
|
+
pass
|
141
|
+
|
142
|
+
|
143
|
+
@dataclass
|
144
|
+
class BackoffRetryDelay(RetryDelayStrategy):
|
145
|
+
multiplier: float
|
146
|
+
min: float
|
147
|
+
max: float
|
148
|
+
|
149
|
+
def __post_init__(self):
|
150
|
+
if self.min > self.max:
|
151
|
+
raise ValueError("`min` greater than `max`")
|
152
|
+
|
153
|
+
def delay(self, times_rejected: int) -> float:
|
154
|
+
retry_delay = self.multiplier * float(times_rejected)
|
155
|
+
|
156
|
+
retry_delay = max(self.min, retry_delay)
|
157
|
+
retry_delay = min(self.max, retry_delay)
|
158
|
+
|
159
|
+
return retry_delay
|
160
|
+
|
161
|
+
|
162
|
+
@dataclass
|
163
|
+
class FixedRetryDelay(RetryDelayStrategy):
|
164
|
+
retry_delay: float
|
165
|
+
|
166
|
+
def delay(self, times_rejected: int) -> float:
|
167
|
+
del times_rejected
|
168
|
+
|
169
|
+
return self.retry_delay
|
170
|
+
|
171
|
+
|
172
|
+
@dataclass
|
173
|
+
class RetryDelayJitter:
|
174
|
+
min: float = 0.5
|
175
|
+
max: float = 1.0
|
176
|
+
|
177
|
+
def __post_init__(self):
|
178
|
+
if self.min > self.max:
|
179
|
+
raise ValueError("`min` greater than `max`")
|
180
|
+
|
181
|
+
|
182
|
+
@dataclass
|
183
|
+
class RetryPolicy:
|
184
|
+
exceptions: Collection[type[Exception]]
|
185
|
+
max_retry: int
|
186
|
+
retry_delay_strategy: RetryDelayStrategy
|
187
|
+
retry_delay_jitter: RetryDelayJitter | None = None
|
188
|
+
|
189
|
+
def can_retry(self, times_rejected: int) -> bool:
|
190
|
+
return times_rejected < self.max_retry
|
191
|
+
|
192
|
+
def next_delay(self, times_rejected: int) -> float:
|
193
|
+
retry_delay = self.retry_delay_strategy.delay(times_rejected)
|
194
|
+
|
195
|
+
if self.retry_delay_jitter is not None:
|
196
|
+
retry_delay = retry_delay + uniform(
|
197
|
+
self.retry_delay_jitter.min, self.retry_delay_jitter.max
|
198
|
+
)
|
199
|
+
|
200
|
+
return retry_delay
|
201
|
+
|
202
|
+
|
203
|
+
@dataclass
|
204
|
+
class ListenerMethodContainer:
|
205
|
+
listener_method: Callable
|
206
|
+
parameters: MappingProxyType[str, Parameter]
|
207
|
+
dependencies: dict[str, type]
|
208
|
+
retry_policy: RetryPolicy | None = None
|
209
|
+
|
210
|
+
|
211
|
+
class ListenerChannelAdapter(BaseChannel):
|
212
|
+
def __init__(
|
213
|
+
self,
|
214
|
+
connection: AsyncioConnection,
|
215
|
+
on_channel_open_callback: Callable[[Channel], None],
|
216
|
+
on_cancel_callback: Callable,
|
217
|
+
):
|
218
|
+
super().__init__(
|
219
|
+
connection=connection,
|
220
|
+
failed_reopen_threshold=None,
|
221
|
+
)
|
222
|
+
|
223
|
+
self._on_channle_open_listener_callback = on_channel_open_callback
|
224
|
+
self._on_listener_cancel_callback = on_cancel_callback
|
225
|
+
|
226
|
+
def _hook_on_channel_opened(self):
|
227
|
+
self._on_channle_open_listener_callback(self.channel)
|
228
|
+
|
229
|
+
def _hook_on_cancelled(self):
|
230
|
+
self._on_listener_cancel_callback()
|
231
|
+
|
232
|
+
|
233
|
+
@dataclass
|
234
|
+
class ListenerMessageMeta:
|
235
|
+
body: bytes
|
236
|
+
method: Basic.Deliver
|
237
|
+
properties: BasicProperties
|
238
|
+
listener_name: str
|
239
|
+
listener_method_container: ListenerMethodContainer
|
240
|
+
listener_start_time: float
|
241
|
+
|
242
|
+
|
243
|
+
class Listener(AsyncEventLoopMixin):
|
244
|
+
LISTENER_SUCCEEDED_COMSUMPTION = Counter(
|
245
|
+
name="listener_succeeded_comsumption",
|
246
|
+
documentation="Listener succeeded comsumption",
|
247
|
+
labelnames=["queue", "listener_name"],
|
248
|
+
)
|
249
|
+
LISTENER_FAILED_COMSUMPTION = Counter(
|
250
|
+
name="listener_failed_comsumption",
|
251
|
+
documentation="Listener failed comsumption",
|
252
|
+
labelnames=["queue", "listener_name", "exception"],
|
253
|
+
)
|
254
|
+
LISTENER_PROCESSING_LATENCY = Summary(
|
255
|
+
name="listener_processing_latency",
|
256
|
+
documentation="Listener processing latency",
|
257
|
+
labelnames=["queue", "listener_name"],
|
258
|
+
)
|
259
|
+
|
260
|
+
def __init__(
|
261
|
+
self,
|
262
|
+
queue: str,
|
263
|
+
listener_name_header_key: str,
|
264
|
+
prefetch_count: int = 250,
|
265
|
+
durable: bool = True,
|
266
|
+
purge_on_startup: bool = False,
|
267
|
+
retry_policy: RetryPolicy | None = None,
|
268
|
+
):
|
269
|
+
self._queue = queue
|
270
|
+
self._listener_name_header_key = listener_name_header_key
|
271
|
+
self._prefetch_count = prefetch_count
|
272
|
+
self._durable = durable
|
273
|
+
self._purge_on_startup = purge_on_startup
|
274
|
+
self._retry_policy = retry_policy
|
275
|
+
self._listeners: dict[str, ListenerMethodContainer] = {}
|
276
|
+
self._logger = LoggerProvider.default().get_logger("rabbitmq.listener")
|
277
|
+
|
278
|
+
@property
|
279
|
+
def queue(self) -> str:
|
280
|
+
return self._queue
|
281
|
+
|
282
|
+
@property
|
283
|
+
def listener_name_header_key(self) -> str:
|
284
|
+
return self._listener_name_header_key
|
285
|
+
|
286
|
+
@property
|
287
|
+
def listeners(self) -> dict[str, ListenerMethodContainer]:
|
288
|
+
return self._listeners
|
289
|
+
|
290
|
+
def add_listener_method(
|
291
|
+
self,
|
292
|
+
listener_name: str | None,
|
293
|
+
listener_method: Callable,
|
294
|
+
retry_policy: RetryPolicy | None = None,
|
295
|
+
):
|
296
|
+
self._register_listener_method(
|
297
|
+
listener_name=listener_name,
|
298
|
+
listener_method=listener_method,
|
299
|
+
parameters=signature(listener_method).parameters,
|
300
|
+
retry_policy=retry_policy,
|
301
|
+
)
|
302
|
+
|
303
|
+
def _register_listener_method(
|
304
|
+
self,
|
305
|
+
listener_name: str | None,
|
306
|
+
listener_method: Callable,
|
307
|
+
parameters: MappingProxyType[str, Parameter],
|
308
|
+
retry_policy: RetryPolicy | None = None,
|
309
|
+
):
|
310
|
+
listener_name = listener_name or "__default__"
|
311
|
+
|
312
|
+
if listener_name in self._listeners:
|
313
|
+
self._logger.warning(
|
314
|
+
"listener with the name `%s` already exists", listener_name
|
315
|
+
)
|
316
|
+
|
317
|
+
dependencies = {}
|
318
|
+
|
319
|
+
for parameter_name, parameter in parameters.items():
|
320
|
+
if parameter.annotation is not Parameter.empty:
|
321
|
+
if parameter.annotation is ListenerContext:
|
322
|
+
continue
|
323
|
+
|
324
|
+
dependency = validate_annotation(parameter=parameter)
|
325
|
+
|
326
|
+
if dependency is not None:
|
327
|
+
dependencies[parameter_name] = dependency
|
328
|
+
|
329
|
+
continue
|
330
|
+
|
331
|
+
TypeAdapterCache.cache_annotation(parameter.annotation)
|
332
|
+
|
333
|
+
self._listeners[listener_name] = ListenerMethodContainer(
|
334
|
+
listener_method=listener_method,
|
335
|
+
parameters=parameters,
|
336
|
+
dependencies=dependencies,
|
337
|
+
retry_policy=retry_policy,
|
338
|
+
)
|
339
|
+
|
340
|
+
async def configure(
|
341
|
+
self,
|
342
|
+
connection: AsyncioConnection,
|
343
|
+
channel_pool: ChannelPool,
|
344
|
+
on_exception_callback: Callable[[ListenerContext, BaseException], bool],
|
345
|
+
container: Container,
|
346
|
+
logstash: BaseLogstashSender,
|
347
|
+
global_retry_policy: RetryPolicy | None = None,
|
348
|
+
):
|
349
|
+
self._connection = connection
|
350
|
+
self._channel_pool = channel_pool
|
351
|
+
self._listener_future = self.loop.create_future()
|
352
|
+
self._on_exception_callback = on_exception_callback
|
353
|
+
self._container = container
|
354
|
+
self._logstash = logstash
|
355
|
+
self._global_retry_policy = global_retry_policy
|
356
|
+
self._listener_channel = ListenerChannelAdapter(
|
357
|
+
connection=connection,
|
358
|
+
on_channel_open_callback=self._on_channel_opened,
|
359
|
+
on_cancel_callback=self._on_cancelled,
|
360
|
+
)
|
361
|
+
_ = await self._listener_channel.open()
|
362
|
+
|
363
|
+
await self._listener_future
|
364
|
+
|
365
|
+
def _on_channel_opened(self, channel: Channel):
|
366
|
+
self._channel = channel
|
367
|
+
self._flow_control = FlowControl(channel=self._channel, loop=self.loop)
|
368
|
+
|
369
|
+
self._declare_queue()
|
370
|
+
|
371
|
+
def _on_cancelled(self):
|
372
|
+
self._declare_queue()
|
373
|
+
|
374
|
+
def _declare_queue(self):
|
375
|
+
try:
|
376
|
+
self._channel.queue_declare(
|
377
|
+
queue=self._queue,
|
378
|
+
durable=self._durable,
|
379
|
+
callback=self._on_queue_declared,
|
380
|
+
)
|
381
|
+
except Exception as e:
|
382
|
+
self._fail_listener(e)
|
383
|
+
|
384
|
+
def _on_queue_declared(self, method: Method):
|
385
|
+
del method
|
386
|
+
|
387
|
+
if self._purge_on_startup:
|
388
|
+
try:
|
389
|
+
self._channel.queue_purge(
|
390
|
+
queue=self._queue,
|
391
|
+
callback=lambda _: self._set_prefetch_count(),
|
392
|
+
)
|
393
|
+
except Exception as e:
|
394
|
+
self._fail_listener(e)
|
395
|
+
else:
|
396
|
+
self._set_prefetch_count()
|
397
|
+
|
398
|
+
def _set_prefetch_count(self):
|
399
|
+
try:
|
400
|
+
self._channel.basic_qos(
|
401
|
+
prefetch_count=self._prefetch_count,
|
402
|
+
callback=lambda _: self._register_listener(),
|
403
|
+
)
|
404
|
+
except Exception as e:
|
405
|
+
self._fail_listener(e)
|
406
|
+
|
407
|
+
def _register_listener(self):
|
408
|
+
try:
|
409
|
+
_ = self._channel.basic_consume(
|
410
|
+
queue=self._queue,
|
411
|
+
auto_ack=True,
|
412
|
+
on_message_callback=self._on_message,
|
413
|
+
)
|
414
|
+
except Exception as e:
|
415
|
+
self._fail_listener(e)
|
416
|
+
|
417
|
+
return
|
418
|
+
|
419
|
+
if not self._listener_future.done():
|
420
|
+
self._listener_future.set_result(None)
|
421
|
+
|
422
|
+
def _fail_listener(self, exception: Exception):
|
423
|
+
if not self._listener_future.done():
|
424
|
+
self._listener_future.set_exception(exception)
|
425
|
+
|
426
|
+
if not self._channel.is_closing or not self._channel.is_closed:
|
427
|
+
self._channel.close()
|
428
|
+
|
429
|
+
def _on_message(
|
430
|
+
self,
|
431
|
+
channel: Channel,
|
432
|
+
method: Basic.Deliver,
|
433
|
+
properties: BasicProperties,
|
434
|
+
body: bytes,
|
435
|
+
):
|
436
|
+
del channel
|
437
|
+
|
438
|
+
if properties.headers is not None:
|
439
|
+
listener_name = (
|
440
|
+
properties.headers.get(self._listener_name_header_key)
|
441
|
+
or "__default__"
|
442
|
+
)
|
443
|
+
else:
|
444
|
+
listener_name = "__default__"
|
445
|
+
|
446
|
+
self._logger.debug(
|
447
|
+
"message recieved from `%s` queue for listener `%s`",
|
448
|
+
self._queue,
|
449
|
+
listener_name,
|
450
|
+
)
|
451
|
+
|
452
|
+
listener_method_container = self._listeners.get(listener_name)
|
453
|
+
|
454
|
+
if listener_method_container is None:
|
455
|
+
self._logstash.error(
|
456
|
+
message=f"no listener registered with the name `{listener_name}` on queue `{self._queue}`",
|
457
|
+
tags=[
|
458
|
+
"RabbitMQ",
|
459
|
+
self._queue,
|
460
|
+
listener_name,
|
461
|
+
],
|
462
|
+
extra={
|
463
|
+
"serviceType": "RabbitMQ",
|
464
|
+
"queue": self._queue,
|
465
|
+
"listenerName": listener_name,
|
466
|
+
},
|
467
|
+
)
|
468
|
+
|
469
|
+
return
|
470
|
+
|
471
|
+
listener_message_meta = ListenerMessageMeta(
|
472
|
+
body=body,
|
473
|
+
method=method,
|
474
|
+
properties=properties,
|
475
|
+
listener_name=listener_name,
|
476
|
+
listener_method_container=listener_method_container,
|
477
|
+
listener_start_time=time(),
|
478
|
+
)
|
479
|
+
|
480
|
+
self.loop.run_in_executor(
|
481
|
+
executor=None,
|
482
|
+
func=partial(self._parse_and_execute, listener_message_meta),
|
483
|
+
).add_done_callback(
|
484
|
+
partial(self._on_submitted_listener_error, listener_message_meta)
|
485
|
+
)
|
486
|
+
|
487
|
+
def _on_submitted_listener_error(
|
488
|
+
self, listener_message_meta: ListenerMessageMeta, future: Future
|
489
|
+
):
|
490
|
+
if future.cancelled():
|
491
|
+
return
|
492
|
+
|
493
|
+
exception = future.exception()
|
494
|
+
|
495
|
+
if exception is not None:
|
496
|
+
self._call_exception_callback(
|
497
|
+
exception=exception,
|
498
|
+
listener_message_meta=listener_message_meta,
|
499
|
+
message=f"error occured while submitting listener callback on listener `{listener_message_meta.listener_name}` and queue `{self._queue}`",
|
500
|
+
)
|
501
|
+
|
502
|
+
def _parse_and_execute(self, listener_message_meta: ListenerMessageMeta):
|
503
|
+
try:
|
504
|
+
listener_method_args, listener_method_kwargs = self._parse_args(
|
505
|
+
listener_message_meta
|
506
|
+
)
|
507
|
+
except Exception as e:
|
508
|
+
self._call_exception_callback(
|
509
|
+
exception=e,
|
510
|
+
listener_message_meta=listener_message_meta,
|
511
|
+
message=f"arguments for listener `{listener_message_meta.listener_name}` in queue `{self._queue}` are not valid",
|
512
|
+
)
|
513
|
+
|
514
|
+
return
|
515
|
+
|
516
|
+
listener_done_callback = partial(
|
517
|
+
self._on_listener_done_executing, listener_message_meta
|
518
|
+
)
|
519
|
+
|
520
|
+
if iscoroutinefunction(
|
521
|
+
listener_message_meta.listener_method_container.listener_method
|
522
|
+
):
|
523
|
+
self.loop.create_task(
|
524
|
+
listener_message_meta.listener_method_container.listener_method(
|
525
|
+
*listener_method_args, **listener_method_kwargs
|
526
|
+
)
|
527
|
+
).add_done_callback(listener_done_callback)
|
528
|
+
else:
|
529
|
+
self.loop.run_in_executor(
|
530
|
+
executor=None,
|
531
|
+
func=partial(
|
532
|
+
listener_message_meta.listener_method_container.listener_method,
|
533
|
+
*listener_method_args,
|
534
|
+
**listener_method_kwargs,
|
535
|
+
),
|
536
|
+
).add_done_callback(listener_done_callback)
|
537
|
+
|
538
|
+
def _parse_args(
|
539
|
+
self, listener_message_meta: ListenerMessageMeta
|
540
|
+
) -> tuple[list, dict]:
|
541
|
+
try:
|
542
|
+
message = from_json(listener_message_meta.body)
|
543
|
+
except:
|
544
|
+
message = listener_message_meta.body.decode()
|
545
|
+
|
546
|
+
assigned_args = []
|
547
|
+
listener_method_args = []
|
548
|
+
listener_method_kwargs = {}
|
549
|
+
next_positional_arg = 0
|
550
|
+
|
551
|
+
if isinstance(message, dict):
|
552
|
+
args = message.get("args")
|
553
|
+
kwargs = message.get("kwargs")
|
554
|
+
|
555
|
+
if isinstance(args, list) and isinstance(kwargs, dict):
|
556
|
+
for (
|
557
|
+
parameter_name,
|
558
|
+
parameter,
|
559
|
+
) in listener_message_meta.listener_method_container.parameters.items():
|
560
|
+
dependency_key = listener_message_meta.listener_method_container.dependencies.get(
|
561
|
+
parameter_name
|
562
|
+
)
|
563
|
+
dependency = None
|
564
|
+
listener_context = None
|
565
|
+
|
566
|
+
if dependency_key is not None:
|
567
|
+
dependency = self._container.resolve(dependency_key)
|
568
|
+
|
569
|
+
if parameter.annotation is not Parameter.empty and (
|
570
|
+
parameter.annotation is ListenerContext
|
571
|
+
or dependency is ListenerContext
|
572
|
+
):
|
573
|
+
listener_context = ListenerContext(
|
574
|
+
queue=self._queue,
|
575
|
+
listener_name=listener_message_meta.listener_name,
|
576
|
+
body=listener_message_meta.body,
|
577
|
+
flow_control=self._flow_control,
|
578
|
+
)
|
579
|
+
|
580
|
+
if (
|
581
|
+
parameter.kind is Parameter.POSITIONAL_ONLY
|
582
|
+
or parameter.kind is Parameter.POSITIONAL_OR_KEYWORD
|
583
|
+
):
|
584
|
+
if (
|
585
|
+
listener_context is not None
|
586
|
+
or dependency is not None
|
587
|
+
):
|
588
|
+
listener_method_args.append(
|
589
|
+
listener_context or dependency
|
590
|
+
)
|
591
|
+
elif next_positional_arg < len(args):
|
592
|
+
listener_method_args.append(
|
593
|
+
self._validate_parameter(
|
594
|
+
parameter=parameter,
|
595
|
+
obj=args[next_positional_arg],
|
596
|
+
)
|
597
|
+
)
|
598
|
+
|
599
|
+
next_positional_arg += 1
|
600
|
+
elif parameter.name in kwargs:
|
601
|
+
listener_method_kwargs[parameter.name] = (
|
602
|
+
self._assign_kwarg(
|
603
|
+
parameter=parameter,
|
604
|
+
assigned_args=assigned_args,
|
605
|
+
listener_context=listener_context,
|
606
|
+
dependency=dependency,
|
607
|
+
kwargs=kwargs,
|
608
|
+
)
|
609
|
+
)
|
610
|
+
elif (
|
611
|
+
parameter.name not in assigned_args
|
612
|
+
and parameter.default is Parameter.empty
|
613
|
+
):
|
614
|
+
raise ValueError(
|
615
|
+
f"argument {parameter.name} has no default"
|
616
|
+
)
|
617
|
+
elif parameter.kind is Parameter.VAR_POSITIONAL:
|
618
|
+
listener_method_args.extend(
|
619
|
+
[
|
620
|
+
self._validate_parameter(
|
621
|
+
parameter=parameter,
|
622
|
+
obj=arg,
|
623
|
+
)
|
624
|
+
for arg in args[next_positional_arg:]
|
625
|
+
]
|
626
|
+
)
|
627
|
+
|
628
|
+
next_positional_arg += len(args[next_positional_arg:])
|
629
|
+
elif (
|
630
|
+
parameter.kind is Parameter.KEYWORD_ONLY
|
631
|
+
or parameter.kind is Parameter.POSITIONAL_OR_KEYWORD
|
632
|
+
):
|
633
|
+
if (
|
634
|
+
listener_context is not None
|
635
|
+
or dependency is not None
|
636
|
+
):
|
637
|
+
listener_method_kwargs[parameter.name] = (
|
638
|
+
listener_context or dependency
|
639
|
+
)
|
640
|
+
elif parameter.name in kwargs:
|
641
|
+
listener_method_kwargs[parameter.name] = (
|
642
|
+
self._assign_kwarg(
|
643
|
+
parameter=parameter,
|
644
|
+
assigned_args=assigned_args,
|
645
|
+
listener_context=listener_context,
|
646
|
+
dependency=dependency,
|
647
|
+
kwargs=kwargs,
|
648
|
+
)
|
649
|
+
)
|
650
|
+
elif (
|
651
|
+
parameter.name not in assigned_args
|
652
|
+
and parameter.default is Parameter.empty
|
653
|
+
):
|
654
|
+
raise ValueError(
|
655
|
+
f"argument {parameter.name} has no default"
|
656
|
+
)
|
657
|
+
elif parameter.kind is Parameter.VAR_KEYWORD:
|
658
|
+
listener_method_kwargs.update(
|
659
|
+
{
|
660
|
+
k: self._validate_parameter(
|
661
|
+
parameter=parameter,
|
662
|
+
obj=v,
|
663
|
+
)
|
664
|
+
for k, v in kwargs.items()
|
665
|
+
if k not in assigned_args
|
666
|
+
}
|
667
|
+
)
|
668
|
+
|
669
|
+
assigned_args.append(parameter.name)
|
670
|
+
|
671
|
+
return listener_method_args, listener_method_kwargs
|
672
|
+
|
673
|
+
message_consumed = False
|
674
|
+
|
675
|
+
for (
|
676
|
+
parameter_name,
|
677
|
+
parameter,
|
678
|
+
) in listener_message_meta.listener_method_container.parameters.items():
|
679
|
+
dependency = listener_message_meta.listener_method_container.dependencies.get(
|
680
|
+
parameter_name
|
681
|
+
)
|
682
|
+
|
683
|
+
if parameter.annotation is not Parameter.empty and (
|
684
|
+
parameter.annotation is ListenerContext
|
685
|
+
or dependency is ListenerContext
|
686
|
+
):
|
687
|
+
listener_method_args.append(
|
688
|
+
ListenerContext(
|
689
|
+
queue=self._queue,
|
690
|
+
listener_name=listener_message_meta.listener_name,
|
691
|
+
body=listener_message_meta.body,
|
692
|
+
flow_control=self._flow_control,
|
693
|
+
)
|
694
|
+
)
|
695
|
+
elif dependency is not None:
|
696
|
+
listener_method_args.append(self._container.resolve(dependency))
|
697
|
+
elif not message_consumed and message is not None:
|
698
|
+
listener_method_args.append(
|
699
|
+
self._validate_parameter(
|
700
|
+
parameter=parameter,
|
701
|
+
obj=message,
|
702
|
+
)
|
703
|
+
)
|
704
|
+
|
705
|
+
message_consumed = True
|
706
|
+
elif parameter.default is Parameter.empty:
|
707
|
+
raise ValueError(f"argument {parameter_name} has no default")
|
708
|
+
|
709
|
+
return listener_method_args, listener_method_kwargs
|
710
|
+
|
711
|
+
def _assign_kwarg(
|
712
|
+
self,
|
713
|
+
parameter: Parameter,
|
714
|
+
assigned_args: list[str],
|
715
|
+
listener_context: ListenerContext | None,
|
716
|
+
dependency: Any | None,
|
717
|
+
kwargs: dict[str, Any],
|
718
|
+
) -> ListenerContext | Any:
|
719
|
+
if parameter.name in assigned_args:
|
720
|
+
raise KeyError(f"argument {parameter.name} already assigned")
|
721
|
+
|
722
|
+
if listener_context is not None or dependency is not None:
|
723
|
+
return listener_context or dependency
|
724
|
+
|
725
|
+
return self._validate_parameter(
|
726
|
+
parameter=parameter,
|
727
|
+
obj=kwargs[parameter.name],
|
728
|
+
)
|
729
|
+
|
730
|
+
def _validate_parameter(self, parameter: Parameter, obj: Any) -> Any:
|
731
|
+
annotation = parameter.annotation
|
732
|
+
|
733
|
+
if annotation is Parameter.empty:
|
734
|
+
return obj
|
735
|
+
|
736
|
+
annotation_type_adapter = TypeAdapterCache.get_type_adapter(annotation)
|
737
|
+
|
738
|
+
return annotation_type_adapter.validate_python(obj)
|
739
|
+
|
740
|
+
def _on_listener_done_executing(
|
741
|
+
self,
|
742
|
+
listener_message_meta: ListenerMessageMeta,
|
743
|
+
task_or_future: Task | Future,
|
744
|
+
):
|
745
|
+
if task_or_future.cancelled():
|
746
|
+
return
|
747
|
+
|
748
|
+
self._observe_listener_time(listener_message_meta)
|
749
|
+
|
750
|
+
exception = task_or_future.exception()
|
751
|
+
|
752
|
+
if exception is not None:
|
753
|
+
if listener_message_meta.properties.reply_to is None:
|
754
|
+
retry_policy = (
|
755
|
+
listener_message_meta.listener_method_container.retry_policy
|
756
|
+
or self._retry_policy
|
757
|
+
or self._global_retry_policy
|
758
|
+
)
|
759
|
+
|
760
|
+
if retry_policy is not None and any(
|
761
|
+
exception_type in retry_policy.exceptions
|
762
|
+
for exception_type in type(exception).mro()
|
763
|
+
):
|
764
|
+
times_rejected = None
|
765
|
+
|
766
|
+
if listener_message_meta.properties.headers is not None:
|
767
|
+
try:
|
768
|
+
times_rejected = int(
|
769
|
+
listener_message_meta.properties.headers.get(
|
770
|
+
"times_rejected"
|
771
|
+
)
|
772
|
+
)
|
773
|
+
except:
|
774
|
+
pass
|
775
|
+
|
776
|
+
if times_rejected is None:
|
777
|
+
times_rejected = 0
|
778
|
+
|
779
|
+
if retry_policy.can_retry(times_rejected):
|
780
|
+
self._reject_message(
|
781
|
+
listener_message_meta=listener_message_meta,
|
782
|
+
retry_policy=retry_policy,
|
783
|
+
times_rejected=times_rejected,
|
784
|
+
)
|
785
|
+
|
786
|
+
return
|
787
|
+
|
788
|
+
self._call_exception_callback(
|
789
|
+
exception=exception,
|
790
|
+
listener_message_meta=listener_message_meta,
|
791
|
+
message=f"error occured while executing listener `{listener_message_meta.listener_name}` in queue `{self._queue}`",
|
792
|
+
)
|
793
|
+
|
794
|
+
if (
|
795
|
+
listener_message_meta.properties.reply_to is not None
|
796
|
+
and exception is None
|
797
|
+
):
|
798
|
+
self._reply_response(
|
799
|
+
listener_message_meta=listener_message_meta,
|
800
|
+
response=task_or_future.result(),
|
801
|
+
)
|
802
|
+
|
803
|
+
self._logger.debug(
|
804
|
+
"message from queue `%s` consumed by listener `%s`",
|
805
|
+
self.queue,
|
806
|
+
listener_message_meta.listener_name,
|
807
|
+
)
|
808
|
+
|
809
|
+
if exception is not None:
|
810
|
+
self.LISTENER_FAILED_COMSUMPTION.labels(
|
811
|
+
queue=self._queue,
|
812
|
+
listener_name=listener_message_meta.listener_name,
|
813
|
+
exception=exception.__class__.__name__,
|
814
|
+
).inc()
|
815
|
+
else:
|
816
|
+
self.LISTENER_SUCCEEDED_COMSUMPTION.labels(
|
817
|
+
queue=self._queue,
|
818
|
+
listener_name=listener_message_meta.listener_name,
|
819
|
+
).inc()
|
820
|
+
|
821
|
+
def _observe_listener_time(
|
822
|
+
self, listener_message_meta: ListenerMessageMeta
|
823
|
+
):
|
824
|
+
self.LISTENER_PROCESSING_LATENCY.labels(
|
825
|
+
queue=self._queue, listener_name=listener_message_meta.listener_name
|
826
|
+
).observe(listener_message_meta.listener_start_time - time())
|
827
|
+
|
828
|
+
def _call_exception_callback(
|
829
|
+
self,
|
830
|
+
exception: BaseException,
|
831
|
+
listener_message_meta: ListenerMessageMeta,
|
832
|
+
message: str | None = None,
|
833
|
+
):
|
834
|
+
context_dispose_callback = None
|
835
|
+
rpc_reply = None
|
836
|
+
|
837
|
+
if listener_message_meta.properties.reply_to is not None:
|
838
|
+
|
839
|
+
def on_context_disposed(context: ListenerContext):
|
840
|
+
assert listener_message_meta.properties.reply_to is not None
|
841
|
+
assert context.rpc_reply is not None
|
842
|
+
|
843
|
+
if not context.rpc_reply.replied:
|
844
|
+
self._reply_response(
|
845
|
+
listener_message_meta=listener_message_meta,
|
846
|
+
response=self._reponse_from_exception(exception),
|
847
|
+
)
|
848
|
+
|
849
|
+
context_dispose_callback = on_context_disposed
|
850
|
+
rpc_reply = RpcReply(
|
851
|
+
channel_pool=self._channel_pool,
|
852
|
+
reply_to=listener_message_meta.properties.reply_to,
|
853
|
+
correlation_id=listener_message_meta.properties.correlation_id,
|
854
|
+
)
|
855
|
+
|
856
|
+
try:
|
857
|
+
exception_callback_succeeded = self._on_exception_callback(
|
858
|
+
ListenerContext(
|
859
|
+
queue=self._queue,
|
860
|
+
listener_name=listener_message_meta.listener_name,
|
861
|
+
body=listener_message_meta.body,
|
862
|
+
flow_control=self._flow_control,
|
863
|
+
rpc_reply=rpc_reply,
|
864
|
+
context_dispose_callback=context_dispose_callback,
|
865
|
+
),
|
866
|
+
exception,
|
867
|
+
)
|
868
|
+
except:
|
869
|
+
self._logstash.exception(
|
870
|
+
message=f"error occured while invoking rabbitmq exception handler callback in listener `{listener_message_meta.listener_name}` and queue `{self._queue}`",
|
871
|
+
tags=[
|
872
|
+
"RabbitMQ",
|
873
|
+
self._queue,
|
874
|
+
listener_message_meta.listener_name,
|
875
|
+
],
|
876
|
+
extra={
|
877
|
+
"serviceType": "RabbitMQ",
|
878
|
+
"queue": self._queue,
|
879
|
+
"listenerName": listener_message_meta.listener_name,
|
880
|
+
"section": "exceptionHandlerCallback",
|
881
|
+
"raisedException": exception.__class__.__name__,
|
882
|
+
},
|
883
|
+
)
|
884
|
+
|
885
|
+
return
|
886
|
+
|
887
|
+
if not exception_callback_succeeded:
|
888
|
+
self._logstash.exception(
|
889
|
+
message=(
|
890
|
+
message
|
891
|
+
or f"error occured while handling event in listener `{listener_message_meta.listener_name}` and queue `{self._queue}`"
|
892
|
+
),
|
893
|
+
tags=[
|
894
|
+
"RabbitMQ",
|
895
|
+
self._queue,
|
896
|
+
listener_message_meta.listener_name,
|
897
|
+
],
|
898
|
+
extra={
|
899
|
+
"serviceType": "RabbitMQ",
|
900
|
+
"queue": self._queue,
|
901
|
+
"listenerName": listener_message_meta.listener_name,
|
902
|
+
"raisedException": exception.__class__.__name__,
|
903
|
+
},
|
904
|
+
)
|
905
|
+
|
906
|
+
def _reject_message(
|
907
|
+
self,
|
908
|
+
listener_message_meta: ListenerMessageMeta,
|
909
|
+
retry_policy: RetryPolicy,
|
910
|
+
times_rejected: int,
|
911
|
+
):
|
912
|
+
message_redelivery_delay = retry_policy.next_delay(times_rejected)
|
913
|
+
|
914
|
+
self._logger.debug(
|
915
|
+
"message will be redelivered to listenr `%s` on queue `%s` after `%f` seconds, times redelivered `%d`",
|
916
|
+
listener_message_meta.listener_name,
|
917
|
+
self._queue,
|
918
|
+
message_redelivery_delay,
|
919
|
+
times_rejected,
|
920
|
+
)
|
921
|
+
self.loop.call_later(
|
922
|
+
delay=message_redelivery_delay,
|
923
|
+
callback=partial(
|
924
|
+
self._on_time_to_redeliver_message,
|
925
|
+
listener_message_meta,
|
926
|
+
times_rejected,
|
927
|
+
),
|
928
|
+
)
|
929
|
+
|
930
|
+
def _on_time_to_redeliver_message(
|
931
|
+
self,
|
932
|
+
listener_message_meta: ListenerMessageMeta,
|
933
|
+
times_rejected: int,
|
934
|
+
):
|
935
|
+
self.loop.create_task(self._channel_pool.get()).add_done_callback(
|
936
|
+
partial(
|
937
|
+
self._on_redelivery_channel_found,
|
938
|
+
listener_message_meta,
|
939
|
+
times_rejected,
|
940
|
+
)
|
941
|
+
)
|
942
|
+
|
943
|
+
def _on_redelivery_channel_found(
|
944
|
+
self,
|
945
|
+
listener_message_meta: ListenerMessageMeta,
|
946
|
+
times_rejected: int,
|
947
|
+
task: Task[BaseChannel],
|
948
|
+
):
|
949
|
+
if task.cancelled():
|
950
|
+
return
|
951
|
+
|
952
|
+
exception = task.exception()
|
953
|
+
|
954
|
+
if exception is not None:
|
955
|
+
self._call_exception_callback(
|
956
|
+
exception=exception,
|
957
|
+
listener_message_meta=listener_message_meta,
|
958
|
+
message=f"error occured while getting channel from pool in listener `{listener_message_meta.listener_name}` and queue `{self._queue}` after rejecteed {times_rejected} times",
|
959
|
+
)
|
960
|
+
|
961
|
+
return
|
962
|
+
|
963
|
+
headers = {
|
964
|
+
self._listener_name_header_key: listener_message_meta.listener_name,
|
965
|
+
"times_rejected": times_rejected + 1,
|
966
|
+
}
|
967
|
+
|
968
|
+
if listener_message_meta.properties.headers is None:
|
969
|
+
listener_message_meta.properties.headers = headers
|
970
|
+
else:
|
971
|
+
listener_message_meta.properties.headers.update(headers)
|
972
|
+
|
973
|
+
try:
|
974
|
+
with task.result() as channel:
|
975
|
+
channel.basic_publish(
|
976
|
+
exchange=DEFAULT_EXCHANGE,
|
977
|
+
routing_key=self._queue,
|
978
|
+
body=listener_message_meta.body,
|
979
|
+
properties=listener_message_meta.properties,
|
980
|
+
)
|
981
|
+
except Exception as e:
|
982
|
+
self._call_exception_callback(
|
983
|
+
exception=e,
|
984
|
+
listener_message_meta=listener_message_meta,
|
985
|
+
message=f"error occured while sending event for redelivery in listener `{listener_message_meta.listener_name}` and queue `{self._queue}` after rejecteed {times_rejected} times",
|
986
|
+
)
|
987
|
+
|
988
|
+
return
|
989
|
+
|
990
|
+
self._logger.debug(
|
991
|
+
"message queued for redelivery to `%s` on queue `%s`, times redelivered `%d`",
|
992
|
+
listener_message_meta.listener_name,
|
993
|
+
self._queue,
|
994
|
+
times_rejected + 1,
|
995
|
+
)
|
996
|
+
|
997
|
+
def _reply_response(
|
998
|
+
self,
|
999
|
+
listener_message_meta: ListenerMessageMeta,
|
1000
|
+
response: Any,
|
1001
|
+
):
|
1002
|
+
assert listener_message_meta.properties.reply_to is not None
|
1003
|
+
|
1004
|
+
reponse_properties = BasicProperties(content_type="application/json")
|
1005
|
+
|
1006
|
+
if listener_message_meta.properties.correlation_id is None:
|
1007
|
+
self._logger.warning(
|
1008
|
+
"`correlation_id` property not supplied for listener `%s` and queue `%s`",
|
1009
|
+
listener_message_meta.listener_name,
|
1010
|
+
self._queue,
|
1011
|
+
)
|
1012
|
+
else:
|
1013
|
+
reponse_properties.correlation_id = (
|
1014
|
+
listener_message_meta.properties.correlation_id
|
1015
|
+
)
|
1016
|
+
|
1017
|
+
try:
|
1018
|
+
response_body = to_json(response)
|
1019
|
+
except Exception as e:
|
1020
|
+
self._call_exception_callback(
|
1021
|
+
exception=e,
|
1022
|
+
listener_message_meta=listener_message_meta,
|
1023
|
+
message=f"listener response is not json serializable in listener `{listener_message_meta.listener_name}` and queue `{self._queue}`",
|
1024
|
+
)
|
1025
|
+
|
1026
|
+
return
|
1027
|
+
|
1028
|
+
self.loop.create_task(self._channel_pool.get()).add_done_callback(
|
1029
|
+
partial(
|
1030
|
+
self._on_reply_channel_found,
|
1031
|
+
listener_message_meta,
|
1032
|
+
response_body,
|
1033
|
+
reponse_properties,
|
1034
|
+
)
|
1035
|
+
)
|
1036
|
+
|
1037
|
+
def _reponse_from_exception(self, exception: BaseException) -> dict:
|
1038
|
+
match exception:
|
1039
|
+
case RabbitMQException() as rabbitmq_exception:
|
1040
|
+
code = rabbitmq_exception.code
|
1041
|
+
message = rabbitmq_exception.message
|
1042
|
+
case ValidationError() as validation_error:
|
1043
|
+
code = 0
|
1044
|
+
message = validation_error.json()
|
1045
|
+
case unknown_execption:
|
1046
|
+
code = 0
|
1047
|
+
message = str(unknown_execption)
|
1048
|
+
|
1049
|
+
return {
|
1050
|
+
"exception": True,
|
1051
|
+
"code": code,
|
1052
|
+
"message": message,
|
1053
|
+
}
|
1054
|
+
|
1055
|
+
def _on_reply_channel_found(
|
1056
|
+
self,
|
1057
|
+
listener_message_meta: ListenerMessageMeta,
|
1058
|
+
response_body: bytes,
|
1059
|
+
response_properties: BasicProperties,
|
1060
|
+
task: Task[BaseChannel],
|
1061
|
+
):
|
1062
|
+
if task.cancelled():
|
1063
|
+
return
|
1064
|
+
|
1065
|
+
exception = task.exception()
|
1066
|
+
|
1067
|
+
if exception is not None:
|
1068
|
+
self._call_exception_callback(
|
1069
|
+
exception=exception,
|
1070
|
+
listener_message_meta=listener_message_meta,
|
1071
|
+
message=f"error occured while getting channel for publishing response in listener `{listener_message_meta.listener_name}` and queue `{self._queue}`",
|
1072
|
+
)
|
1073
|
+
|
1074
|
+
return
|
1075
|
+
|
1076
|
+
try:
|
1077
|
+
with task.result() as channel:
|
1078
|
+
channel.basic_publish(
|
1079
|
+
exchange=DEFAULT_EXCHANGE,
|
1080
|
+
routing_key=listener_message_meta.properties.reply_to,
|
1081
|
+
properties=response_properties,
|
1082
|
+
body=response_body,
|
1083
|
+
)
|
1084
|
+
except Exception as e:
|
1085
|
+
self._call_exception_callback(
|
1086
|
+
exception=e,
|
1087
|
+
listener_message_meta=listener_message_meta,
|
1088
|
+
message=f"error occured while publishing response to rpc call in listener `{listener_message_meta.listener_name}` and queue `{self._queue}`",
|
1089
|
+
)
|
1090
|
+
|
1091
|
+
return
|
1092
|
+
|
1093
|
+
self._logger.debug(
|
1094
|
+
"sent a reply to `%s` for request from queue `%s` and listener `%s`",
|
1095
|
+
listener_message_meta.properties.reply_to,
|
1096
|
+
self._queue,
|
1097
|
+
listener_message_meta.listener_name,
|
1098
|
+
)
|
1099
|
+
|
1100
|
+
def __call__(self, listener: type[L]) -> type[L]:
|
1101
|
+
setattr(listener, LISTENER_ATTRIBUTE, self)
|
1102
|
+
|
1103
|
+
return listener
|
1104
|
+
|
1105
|
+
|
1106
|
+
class Consumer(Listener):
|
1107
|
+
def __init__(
|
1108
|
+
self,
|
1109
|
+
queue: str,
|
1110
|
+
prefetch_count: int = 250,
|
1111
|
+
retry_policy: RetryPolicy | None = None,
|
1112
|
+
):
|
1113
|
+
super().__init__(
|
1114
|
+
queue=queue,
|
1115
|
+
listener_name_header_key="target",
|
1116
|
+
prefetch_count=prefetch_count,
|
1117
|
+
retry_policy=retry_policy,
|
1118
|
+
)
|
1119
|
+
|
1120
|
+
def consume(
|
1121
|
+
self, target: str | None = None, retry_policy: RetryPolicy | None = None
|
1122
|
+
) -> Callable[[Callable], Callable]:
|
1123
|
+
def wrapper(consumer_method: Callable):
|
1124
|
+
if not callable(consumer_method):
|
1125
|
+
raise TypeError(
|
1126
|
+
f"consumer method argument not a callable, got {type(consumer_method)}"
|
1127
|
+
)
|
1128
|
+
|
1129
|
+
self._register_listener_method(
|
1130
|
+
listener_name=target,
|
1131
|
+
listener_method=consumer_method,
|
1132
|
+
parameters=signature(consumer_method).parameters,
|
1133
|
+
retry_policy=retry_policy,
|
1134
|
+
)
|
1135
|
+
|
1136
|
+
return consumer_method
|
1137
|
+
|
1138
|
+
return wrapper
|
1139
|
+
|
1140
|
+
|
1141
|
+
class RpcWorker(Listener):
|
1142
|
+
def __init__(self, queue: str, prefetch_count: int = 250):
|
1143
|
+
super().__init__(
|
1144
|
+
queue=queue,
|
1145
|
+
listener_name_header_key="procedure",
|
1146
|
+
prefetch_count=prefetch_count,
|
1147
|
+
purge_on_startup=True,
|
1148
|
+
)
|
1149
|
+
|
1150
|
+
def execute(
|
1151
|
+
self, procedure: str | None = None
|
1152
|
+
) -> Callable[[Callable], Callable]:
|
1153
|
+
def wrapper(worker_method: Callable):
|
1154
|
+
if not callable(worker_method):
|
1155
|
+
raise TypeError(
|
1156
|
+
f"worker method argument not a callable, got {type(worker_method)}"
|
1157
|
+
)
|
1158
|
+
|
1159
|
+
self._register_listener_method(
|
1160
|
+
listener_name=procedure,
|
1161
|
+
listener_method=worker_method,
|
1162
|
+
parameters=signature(worker_method).parameters,
|
1163
|
+
)
|
1164
|
+
|
1165
|
+
return worker_method
|
1166
|
+
|
1167
|
+
return wrapper
|
1168
|
+
|
1169
|
+
|
1170
|
+
@dataclass
|
1171
|
+
class ListenerMethodMeta:
|
1172
|
+
listener_name: str | None = None
|
1173
|
+
retry_policy: RetryPolicy | None = None
|
1174
|
+
|
1175
|
+
|
1176
|
+
class ListenerBase:
|
1177
|
+
def get_inner_listener(self) -> Listener:
|
1178
|
+
listener = getattr(self, LISTENER_ATTRIBUTE, None)
|
1179
|
+
|
1180
|
+
if listener is None or not isinstance(listener, Listener):
|
1181
|
+
raise TypeError(
|
1182
|
+
f"{self.__class__.__name__} not a listener, possibly no annotated with either `Consumer` or `RpcWorker`"
|
1183
|
+
)
|
1184
|
+
|
1185
|
+
return listener
|
1186
|
+
|
1187
|
+
def register_listener_methods(self) -> Listener:
|
1188
|
+
listener = self.get_inner_listener()
|
1189
|
+
listener_method_attribute = None
|
1190
|
+
|
1191
|
+
for attribute_name in dir(self):
|
1192
|
+
attribute = getattr(self, attribute_name, None)
|
1193
|
+
|
1194
|
+
if attribute is None:
|
1195
|
+
continue
|
1196
|
+
|
1197
|
+
listener_method_attribute, listener_method, listener_method_meta = (
|
1198
|
+
self._validate_listener_method_attribute(
|
1199
|
+
attribute=attribute,
|
1200
|
+
listener_method_attribute=CONSUMER_ATTRIBUTE,
|
1201
|
+
previous_listener_method_attribute=listener_method_attribute,
|
1202
|
+
)
|
1203
|
+
)
|
1204
|
+
|
1205
|
+
if listener_method is None or listener_method_meta is None:
|
1206
|
+
(
|
1207
|
+
listener_method_attribute,
|
1208
|
+
listener_method,
|
1209
|
+
listener_method_meta,
|
1210
|
+
) = self._validate_listener_method_attribute(
|
1211
|
+
attribute=attribute,
|
1212
|
+
listener_method_attribute=RPC_WORKER_ATTRIBUTE,
|
1213
|
+
previous_listener_method_attribute=listener_method_attribute,
|
1214
|
+
)
|
1215
|
+
|
1216
|
+
if listener_method is None or listener_method_meta is None:
|
1217
|
+
continue
|
1218
|
+
|
1219
|
+
listener.add_listener_method(
|
1220
|
+
listener_name=listener_method_meta.listener_name,
|
1221
|
+
listener_method=listener_method,
|
1222
|
+
retry_policy=listener_method_meta.retry_policy,
|
1223
|
+
)
|
1224
|
+
|
1225
|
+
return listener
|
1226
|
+
|
1227
|
+
def _validate_listener_method_attribute(
|
1228
|
+
self,
|
1229
|
+
attribute: Any,
|
1230
|
+
listener_method_attribute: str,
|
1231
|
+
previous_listener_method_attribute: str | None,
|
1232
|
+
) -> tuple[str | None, Callable | None, ListenerMethodMeta | None]:
|
1233
|
+
listener_method_meta = getattr(
|
1234
|
+
attribute, listener_method_attribute, None
|
1235
|
+
)
|
1236
|
+
|
1237
|
+
if listener_method_meta is None:
|
1238
|
+
return previous_listener_method_attribute or None, None, None
|
1239
|
+
|
1240
|
+
if not isinstance(listener_method_meta, ListenerMethodMeta):
|
1241
|
+
raise TypeError(
|
1242
|
+
f"expected `{listener_method_attribute}` to by of type `ListenerMethodMeta`, got {type(listener_method_meta)}"
|
1243
|
+
)
|
1244
|
+
|
1245
|
+
if (
|
1246
|
+
previous_listener_method_attribute is not None
|
1247
|
+
and listener_method_attribute != previous_listener_method_attribute
|
1248
|
+
):
|
1249
|
+
raise ValueError("listener methods cannot be of different type")
|
1250
|
+
|
1251
|
+
if not callable(attribute):
|
1252
|
+
raise TypeError(
|
1253
|
+
f"object annotated with `{listener_method_attribute}` is not callable"
|
1254
|
+
)
|
1255
|
+
|
1256
|
+
return listener_method_attribute, attribute, listener_method_meta
|
1257
|
+
|
1258
|
+
|
1259
|
+
def consume(
|
1260
|
+
target: str | None = None, retry_policy: RetryPolicy | None = None
|
1261
|
+
) -> Callable[[Callable], Callable]:
|
1262
|
+
def wrapper(consumer_method: Callable) -> Callable:
|
1263
|
+
if not callable(consumer_method):
|
1264
|
+
raise TypeError(
|
1265
|
+
f"consumer method argument not a callable, got {type(consumer_method)}"
|
1266
|
+
)
|
1267
|
+
|
1268
|
+
setattr(
|
1269
|
+
consumer_method,
|
1270
|
+
CONSUMER_ATTRIBUTE,
|
1271
|
+
ListenerMethodMeta(listener_name=target, retry_policy=retry_policy),
|
1272
|
+
)
|
1273
|
+
|
1274
|
+
return consumer_method
|
1275
|
+
|
1276
|
+
return wrapper
|
1277
|
+
|
1278
|
+
|
1279
|
+
def execute(procedure: str | None = None) -> Callable[[Callable], Callable]:
|
1280
|
+
def wrapper(worker_method: Callable) -> Callable:
|
1281
|
+
if not callable(worker_method):
|
1282
|
+
raise TypeError(
|
1283
|
+
f"worker method argument not a callable, got {type(worker_method)}"
|
1284
|
+
)
|
1285
|
+
|
1286
|
+
setattr(
|
1287
|
+
worker_method, RPC_WORKER_ATTRIBUTE, ListenerMethodMeta(procedure)
|
1288
|
+
)
|
1289
|
+
|
1290
|
+
return worker_method
|
1291
|
+
|
1292
|
+
return wrapper
|