jararaca 0.3.11a9__py3-none-any.whl → 0.3.11a11__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.
Potentially problematic release.
This version of jararaca might be problematic. Click here for more details.
- jararaca/__init__.py +3 -3
- jararaca/cli.py +19 -143
- jararaca/messagebus/worker.py +260 -39
- jararaca/scheduler/{scheduler_v2.py → beat_worker.py} +11 -23
- jararaca/utils/rabbitmq_utils.py +5 -0
- {jararaca-0.3.11a9.dist-info → jararaca-0.3.11a11.dist-info}/METADATA +1 -1
- {jararaca-0.3.11a9.dist-info → jararaca-0.3.11a11.dist-info}/RECORD +10 -12
- jararaca/messagebus/worker_v2.py +0 -644
- jararaca/scheduler/scheduler.py +0 -181
- {jararaca-0.3.11a9.dist-info → jararaca-0.3.11a11.dist-info}/LICENSE +0 -0
- {jararaca-0.3.11a9.dist-info → jararaca-0.3.11a11.dist-info}/WHEEL +0 -0
- {jararaca-0.3.11a9.dist-info → jararaca-0.3.11a11.dist-info}/entry_points.txt +0 -0
jararaca/messagebus/worker.py
CHANGED
|
@@ -2,15 +2,21 @@ import asyncio
|
|
|
2
2
|
import inspect
|
|
3
3
|
import logging
|
|
4
4
|
import signal
|
|
5
|
+
from abc import ABC
|
|
5
6
|
from contextlib import asynccontextmanager, suppress
|
|
6
7
|
from dataclasses import dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
7
9
|
from typing import Any, AsyncContextManager, AsyncGenerator, Type, get_origin
|
|
10
|
+
from urllib.parse import parse_qs, urlparse
|
|
8
11
|
|
|
9
12
|
import aio_pika
|
|
10
13
|
import aio_pika.abc
|
|
11
14
|
import uvloop
|
|
15
|
+
from aio_pika.exceptions import AMQPError, ChannelClosed, ChannelNotFoundEntity
|
|
12
16
|
from pydantic import BaseModel
|
|
13
17
|
|
|
18
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
19
|
+
from jararaca.broker_backend.mapper import get_message_broker_backend_from_url
|
|
14
20
|
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
15
21
|
from jararaca.di import Container
|
|
16
22
|
from jararaca.lifecycle import AppLifecycle
|
|
@@ -20,16 +26,20 @@ from jararaca.messagebus.bus_message_controller import (
|
|
|
20
26
|
)
|
|
21
27
|
from jararaca.messagebus.decorators import (
|
|
22
28
|
MESSAGE_HANDLER_DATA_SET,
|
|
29
|
+
SCHEDULED_ACTION_DATA_SET,
|
|
23
30
|
MessageBusController,
|
|
24
31
|
MessageHandler,
|
|
25
32
|
MessageHandlerData,
|
|
33
|
+
ScheduleDispatchData,
|
|
26
34
|
)
|
|
27
35
|
from jararaca.messagebus.message import Message, MessageOf
|
|
28
36
|
from jararaca.microservice import (
|
|
29
37
|
AppTransactionContext,
|
|
30
38
|
MessageBusTransactionData,
|
|
31
39
|
Microservice,
|
|
40
|
+
SchedulerTransactionData,
|
|
32
41
|
)
|
|
42
|
+
from jararaca.scheduler.decorators import ScheduledActionData
|
|
33
43
|
from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
34
44
|
|
|
35
45
|
logger = logging.getLogger(__name__)
|
|
@@ -80,15 +90,28 @@ class MessageProcessingLocker:
|
|
|
80
90
|
await asyncio.gather(*self.current_processing_messages_set)
|
|
81
91
|
|
|
82
92
|
|
|
83
|
-
class
|
|
93
|
+
class MessageBusConsumer(ABC):
|
|
94
|
+
|
|
95
|
+
async def consume(self) -> None:
|
|
96
|
+
raise NotImplementedError("consume method not implemented")
|
|
97
|
+
|
|
98
|
+
def shutdown(self) -> None: ...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AioPikaMicroserviceConsumer(MessageBusConsumer):
|
|
84
102
|
def __init__(
|
|
85
103
|
self,
|
|
104
|
+
broker_backend: MessageBrokerBackend,
|
|
86
105
|
config: AioPikaWorkerConfig,
|
|
87
106
|
message_handler_set: MESSAGE_HANDLER_DATA_SET,
|
|
107
|
+
scheduled_actions: SCHEDULED_ACTION_DATA_SET,
|
|
88
108
|
uow_context_provider: UnitOfWorkContextProvider,
|
|
89
109
|
):
|
|
110
|
+
|
|
111
|
+
self.broker_backend = broker_backend
|
|
90
112
|
self.config = config
|
|
91
113
|
self.message_handler_set = message_handler_set
|
|
114
|
+
self.scheduled_actions = scheduled_actions
|
|
92
115
|
self.incoming_map: dict[str, MessageHandlerData] = {}
|
|
93
116
|
self.uow_context_provider = uow_context_provider
|
|
94
117
|
self.shutdown_event = asyncio.Event()
|
|
@@ -96,40 +119,44 @@ class AioPikaMicroserviceConsumer:
|
|
|
96
119
|
self.tasks: set[asyncio.Task[Any]] = set()
|
|
97
120
|
|
|
98
121
|
async def consume(self) -> None:
|
|
122
|
+
|
|
99
123
|
connection = await aio_pika.connect(self.config.url)
|
|
124
|
+
|
|
100
125
|
channel = await connection.channel()
|
|
126
|
+
|
|
101
127
|
await channel.set_qos(prefetch_count=self.config.prefetch_count)
|
|
102
128
|
|
|
103
|
-
# Get existing exchange and
|
|
129
|
+
# Get existing exchange and queues
|
|
104
130
|
try:
|
|
105
|
-
|
|
131
|
+
exchange = await RabbitmqUtils.get_main_exchange(
|
|
106
132
|
channel=channel,
|
|
107
133
|
exchange_name=self.config.exchange,
|
|
108
134
|
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
|
|
136
|
+
dlx = await RabbitmqUtils.get_dl_exchange(channel=channel)
|
|
137
|
+
dlq = await RabbitmqUtils.get_dl_queue(channel=channel)
|
|
138
|
+
except (ChannelNotFoundEntity, ChannelClosed, AMQPError) as e:
|
|
139
|
+
logger.critical(
|
|
140
|
+
f"Required exchange or queue infrastructure not found and passive mode is enabled. "
|
|
113
141
|
f"Please use the declare command first to create the required infrastructure. Error: {e}"
|
|
114
142
|
)
|
|
115
143
|
self.shutdown_event.set()
|
|
116
144
|
return
|
|
117
145
|
|
|
118
146
|
for handler in self.message_handler_set:
|
|
147
|
+
|
|
119
148
|
queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.instance_callable.__module__}.{handler.instance_callable.__qualname__}"
|
|
120
149
|
routing_key = f"{handler.message_type.MESSAGE_TOPIC}.#"
|
|
121
150
|
|
|
122
151
|
self.incoming_map[queue_name] = handler
|
|
123
152
|
|
|
124
|
-
# Get existing queue
|
|
125
153
|
try:
|
|
126
|
-
queue = await RabbitmqUtils.
|
|
127
|
-
channel=channel,
|
|
128
|
-
queue_name=queue_name,
|
|
154
|
+
queue = await RabbitmqUtils.get_queue(
|
|
155
|
+
channel=channel, queue_name=queue_name
|
|
129
156
|
)
|
|
130
|
-
except
|
|
157
|
+
except (ChannelNotFoundEntity, ChannelClosed, AMQPError) as e:
|
|
131
158
|
logger.error(
|
|
132
|
-
f"
|
|
159
|
+
f"Queue '{queue_name}' not found and passive mode is enabled. "
|
|
133
160
|
f"Please use the declare command first to create the queue. Error: {e}"
|
|
134
161
|
)
|
|
135
162
|
continue
|
|
@@ -144,7 +171,36 @@ class AioPikaMicroserviceConsumer:
|
|
|
144
171
|
no_ack=handler.spec.auto_ack,
|
|
145
172
|
)
|
|
146
173
|
|
|
147
|
-
logger.info(f"Consuming {queue_name}")
|
|
174
|
+
logger.info(f"Consuming message handler {queue_name}")
|
|
175
|
+
|
|
176
|
+
for scheduled_action in self.scheduled_actions:
|
|
177
|
+
|
|
178
|
+
queue_name = f"{scheduled_action.callable.__module__}.{scheduled_action.callable.__qualname__}"
|
|
179
|
+
|
|
180
|
+
routing_key = queue_name
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
queue = await RabbitmqUtils.get_queue(
|
|
184
|
+
channel=channel, queue_name=queue_name
|
|
185
|
+
)
|
|
186
|
+
except (ChannelNotFoundEntity, ChannelClosed, AMQPError) as e:
|
|
187
|
+
logger.error(
|
|
188
|
+
f"Scheduler queue '{queue_name}' not found and passive mode is enabled. "
|
|
189
|
+
f"Please use the declare command first to create the queue. Error: {e}"
|
|
190
|
+
)
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
await queue.consume(
|
|
194
|
+
callback=ScheduledMessageHandlerCallback(
|
|
195
|
+
consumer=self,
|
|
196
|
+
queue_name=queue_name,
|
|
197
|
+
routing_key=routing_key,
|
|
198
|
+
scheduled_action=scheduled_action,
|
|
199
|
+
),
|
|
200
|
+
no_ack=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
logger.info(f"Consuming scheduler {queue_name}")
|
|
148
204
|
|
|
149
205
|
await self.shutdown_event.wait()
|
|
150
206
|
logger.info("Worker shutting down")
|
|
@@ -158,6 +214,157 @@ class AioPikaMicroserviceConsumer:
|
|
|
158
214
|
async with self.lock:
|
|
159
215
|
await asyncio.gather(*self.tasks)
|
|
160
216
|
|
|
217
|
+
def shutdown(self) -> None:
|
|
218
|
+
self.shutdown_event.set()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_message_bus(
|
|
222
|
+
broker_url: str,
|
|
223
|
+
broker_backend: MessageBrokerBackend,
|
|
224
|
+
scheduled_actions: SCHEDULED_ACTION_DATA_SET,
|
|
225
|
+
message_handler_set: MESSAGE_HANDLER_DATA_SET,
|
|
226
|
+
uow_context_provider: UnitOfWorkContextProvider,
|
|
227
|
+
) -> MessageBusConsumer:
|
|
228
|
+
|
|
229
|
+
parsed_url = urlparse(broker_url)
|
|
230
|
+
|
|
231
|
+
if parsed_url.scheme == "amqp" or parsed_url.scheme == "amqps":
|
|
232
|
+
assert parsed_url.query, "Query string must be set for AMQP URLs"
|
|
233
|
+
|
|
234
|
+
query_params: dict[str, list[str]] = parse_qs(parsed_url.query)
|
|
235
|
+
|
|
236
|
+
assert "exchange" in query_params, "Exchange must be set in the query string"
|
|
237
|
+
assert (
|
|
238
|
+
len(query_params["exchange"]) == 1
|
|
239
|
+
), "Exchange must be set in the query string"
|
|
240
|
+
assert (
|
|
241
|
+
"prefetch_count" in query_params
|
|
242
|
+
), "Prefetch count must be set in the query string"
|
|
243
|
+
assert (
|
|
244
|
+
len(query_params["prefetch_count"]) == 1
|
|
245
|
+
), "Prefetch count must be set in the query string"
|
|
246
|
+
assert query_params["prefetch_count"][
|
|
247
|
+
0
|
|
248
|
+
].isdigit(), "Prefetch count must be an integer in the query string"
|
|
249
|
+
assert query_params["exchange"][0], "Exchange must be set in the query string"
|
|
250
|
+
assert query_params["prefetch_count"][
|
|
251
|
+
0
|
|
252
|
+
], "Prefetch count must be set in the query string"
|
|
253
|
+
|
|
254
|
+
exchange = query_params["exchange"][0]
|
|
255
|
+
prefetch_count = int(query_params["prefetch_count"][0])
|
|
256
|
+
|
|
257
|
+
config = AioPikaWorkerConfig(
|
|
258
|
+
url=broker_url,
|
|
259
|
+
exchange=exchange,
|
|
260
|
+
prefetch_count=prefetch_count,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return AioPikaMicroserviceConsumer(
|
|
264
|
+
config=config,
|
|
265
|
+
broker_backend=broker_backend,
|
|
266
|
+
message_handler_set=message_handler_set,
|
|
267
|
+
scheduled_actions=scheduled_actions,
|
|
268
|
+
uow_context_provider=uow_context_provider,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"Unsupported broker URL scheme: {parsed_url.scheme}. Supported schemes are amqp and amqps"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ScheduledMessageHandlerCallback:
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
consumer: AioPikaMicroserviceConsumer,
|
|
280
|
+
queue_name: str,
|
|
281
|
+
routing_key: str,
|
|
282
|
+
scheduled_action: ScheduledActionData,
|
|
283
|
+
):
|
|
284
|
+
self.consumer = consumer
|
|
285
|
+
self.queue_name = queue_name
|
|
286
|
+
self.routing_key = routing_key
|
|
287
|
+
self.scheduled_action = scheduled_action
|
|
288
|
+
|
|
289
|
+
async def __call__(
|
|
290
|
+
self, aio_pika_message: aio_pika.abc.AbstractIncomingMessage
|
|
291
|
+
) -> None:
|
|
292
|
+
|
|
293
|
+
if self.consumer.shutdown_event.is_set():
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
async with self.consumer.lock:
|
|
297
|
+
task = asyncio.create_task(self.handle_message(aio_pika_message))
|
|
298
|
+
self.consumer.tasks.add(task)
|
|
299
|
+
task.add_done_callback(self.handle_message_consume_done)
|
|
300
|
+
|
|
301
|
+
def handle_message_consume_done(self, task: asyncio.Task[Any]) -> None:
|
|
302
|
+
self.consumer.tasks.discard(task)
|
|
303
|
+
|
|
304
|
+
async def handle_message(
|
|
305
|
+
self, aio_pika_message: aio_pika.abc.AbstractIncomingMessage
|
|
306
|
+
) -> None:
|
|
307
|
+
|
|
308
|
+
if self.consumer.shutdown_event.is_set():
|
|
309
|
+
logger.info("Shutdown event set. Rqueuing message")
|
|
310
|
+
await aio_pika_message.reject(requeue=True)
|
|
311
|
+
|
|
312
|
+
sig = inspect.signature(self.scheduled_action.callable)
|
|
313
|
+
if len(sig.parameters) == 1:
|
|
314
|
+
|
|
315
|
+
task = asyncio.create_task(
|
|
316
|
+
self.run_with_context(
|
|
317
|
+
self.scheduled_action,
|
|
318
|
+
(ScheduleDispatchData(int(aio_pika_message.body.decode("utf-8"))),),
|
|
319
|
+
{},
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
elif len(sig.parameters) == 0:
|
|
324
|
+
task = asyncio.create_task(
|
|
325
|
+
self.run_with_context(
|
|
326
|
+
self.scheduled_action,
|
|
327
|
+
(),
|
|
328
|
+
{},
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
logger.warning(
|
|
333
|
+
"Scheduled action '%s' must have exactly one parameter of type ScheduleDispatchData or no parameters"
|
|
334
|
+
% self.queue_name
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
self.consumer.tasks.add(task)
|
|
339
|
+
task.add_done_callback(self.handle_message_consume_done)
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
await task
|
|
343
|
+
except Exception as e:
|
|
344
|
+
|
|
345
|
+
logger.exception(
|
|
346
|
+
f"Error processing scheduled action {self.queue_name}: {e}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
async def run_with_context(
|
|
350
|
+
self,
|
|
351
|
+
scheduled_action: ScheduledActionData,
|
|
352
|
+
args: tuple[Any, ...],
|
|
353
|
+
kwargs: dict[str, Any],
|
|
354
|
+
) -> None:
|
|
355
|
+
async with self.consumer.uow_context_provider(
|
|
356
|
+
AppTransactionContext(
|
|
357
|
+
controller_member_reflect=scheduled_action.controller_member,
|
|
358
|
+
transaction_data=SchedulerTransactionData(
|
|
359
|
+
scheduled_to=datetime.now(UTC),
|
|
360
|
+
cron_expression=scheduled_action.spec.cron,
|
|
361
|
+
triggered_at=datetime.now(UTC),
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
):
|
|
365
|
+
|
|
366
|
+
await scheduled_action.callable(*args, **kwargs)
|
|
367
|
+
|
|
161
368
|
|
|
162
369
|
class MessageHandlerCallback:
|
|
163
370
|
|
|
@@ -220,13 +427,7 @@ class MessageHandlerCallback:
|
|
|
220
427
|
await self.handle_reject_message(aio_pika_message)
|
|
221
428
|
return
|
|
222
429
|
|
|
223
|
-
handler_data = self.
|
|
224
|
-
|
|
225
|
-
if handler_data is None:
|
|
226
|
-
logger.warning("No handler found for topic '%s'" % routing_key)
|
|
227
|
-
await self.handle_reject_message(aio_pika_message)
|
|
228
|
-
|
|
229
|
-
return
|
|
430
|
+
handler_data = self.message_handler
|
|
230
431
|
|
|
231
432
|
handler = handler_data.instance_callable
|
|
232
433
|
|
|
@@ -324,9 +525,18 @@ async def none_context() -> AsyncGenerator[None, None]:
|
|
|
324
525
|
|
|
325
526
|
|
|
326
527
|
class MessageBusWorker:
|
|
327
|
-
def __init__(
|
|
528
|
+
def __init__(
|
|
529
|
+
self,
|
|
530
|
+
app: Microservice,
|
|
531
|
+
broker_url: str,
|
|
532
|
+
backend_url: str,
|
|
533
|
+
handler_names: set[str] | None = None,
|
|
534
|
+
) -> None:
|
|
328
535
|
self.app = app
|
|
329
|
-
self.
|
|
536
|
+
self.backend_url = backend_url
|
|
537
|
+
self.broker_url = broker_url
|
|
538
|
+
self.handler_names = handler_names
|
|
539
|
+
|
|
330
540
|
self.container = Container(app)
|
|
331
541
|
self.lifecycle = AppLifecycle(app, self.container)
|
|
332
542
|
|
|
@@ -334,39 +544,46 @@ class MessageBusWorker:
|
|
|
334
544
|
app=app, container=self.container
|
|
335
545
|
)
|
|
336
546
|
|
|
337
|
-
self._consumer:
|
|
547
|
+
self._consumer: MessageBusConsumer | None = None
|
|
338
548
|
|
|
339
549
|
@property
|
|
340
|
-
def consumer(self) ->
|
|
550
|
+
def consumer(self) -> MessageBusConsumer:
|
|
341
551
|
if self._consumer is None:
|
|
342
552
|
raise RuntimeError("Consumer not started")
|
|
343
553
|
return self._consumer
|
|
344
554
|
|
|
345
|
-
async def start_async(self
|
|
555
|
+
async def start_async(self) -> None:
|
|
346
556
|
all_message_handlers_set: MESSAGE_HANDLER_DATA_SET = set()
|
|
557
|
+
all_scheduled_actions_set: SCHEDULED_ACTION_DATA_SET = set()
|
|
347
558
|
async with self.lifecycle():
|
|
348
|
-
for
|
|
349
|
-
controller = MessageBusController.get_messagebus(
|
|
559
|
+
for instance_class in self.app.controllers:
|
|
560
|
+
controller = MessageBusController.get_messagebus(instance_class)
|
|
350
561
|
|
|
351
562
|
if controller is None:
|
|
352
563
|
continue
|
|
353
564
|
|
|
354
|
-
instance: Any = self.container.get_by_type(
|
|
565
|
+
instance: Any = self.container.get_by_type(instance_class)
|
|
355
566
|
|
|
356
567
|
factory = controller.get_messagebus_factory()
|
|
357
|
-
handlers,
|
|
568
|
+
handlers, schedulers = factory(instance)
|
|
358
569
|
|
|
359
570
|
message_handler_data_map: dict[str, MessageHandlerData] = {}
|
|
360
|
-
|
|
571
|
+
all_scheduled_actions_set.update(schedulers)
|
|
361
572
|
for handler_data in handlers:
|
|
362
573
|
message_type = handler_data.spec.message_type
|
|
363
574
|
topic = message_type.MESSAGE_TOPIC
|
|
364
575
|
|
|
365
576
|
# Filter handlers by name if specified
|
|
366
|
-
if
|
|
367
|
-
|
|
577
|
+
if (
|
|
578
|
+
self.handler_names is not None
|
|
579
|
+
and handler_data.spec.name is not None
|
|
580
|
+
):
|
|
581
|
+
if handler_data.spec.name not in self.handler_names:
|
|
368
582
|
continue
|
|
369
|
-
elif
|
|
583
|
+
elif (
|
|
584
|
+
self.handler_names is not None
|
|
585
|
+
and handler_data.spec.name is None
|
|
586
|
+
):
|
|
370
587
|
# Skip handlers without names when filtering is requested
|
|
371
588
|
continue
|
|
372
589
|
|
|
@@ -382,25 +599,29 @@ class MessageBusWorker:
|
|
|
382
599
|
message_handler_data_map[topic] = handler_data
|
|
383
600
|
all_message_handlers_set.add(handler_data)
|
|
384
601
|
|
|
385
|
-
|
|
386
|
-
|
|
602
|
+
broker_backend = get_message_broker_backend_from_url(url=self.backend_url)
|
|
603
|
+
|
|
604
|
+
consumer = self._consumer = create_message_bus(
|
|
605
|
+
broker_url=self.broker_url,
|
|
606
|
+
broker_backend=broker_backend,
|
|
607
|
+
scheduled_actions=all_scheduled_actions_set,
|
|
387
608
|
message_handler_set=all_message_handlers_set,
|
|
388
609
|
uow_context_provider=self.uow_context_provider,
|
|
389
610
|
)
|
|
390
611
|
|
|
391
612
|
await consumer.consume()
|
|
392
613
|
|
|
393
|
-
def start_sync(self
|
|
614
|
+
def start_sync(self) -> None:
|
|
394
615
|
|
|
395
616
|
def on_shutdown(loop: asyncio.AbstractEventLoop) -> None:
|
|
396
617
|
logger.info("Shutting down")
|
|
397
|
-
self.consumer.
|
|
618
|
+
self.consumer.shutdown()
|
|
398
619
|
|
|
399
620
|
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
|
|
400
621
|
runner.get_loop().add_signal_handler(
|
|
401
622
|
signal.SIGINT, on_shutdown, runner.get_loop()
|
|
402
623
|
)
|
|
403
|
-
runner.run(self.start_async(
|
|
624
|
+
runner.run(self.start_async())
|
|
404
625
|
|
|
405
626
|
|
|
406
627
|
class AioPikaMessageBusController(BusMessageController):
|
|
@@ -4,10 +4,9 @@ import logging
|
|
|
4
4
|
import signal
|
|
5
5
|
import time
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
-
from contextlib import asynccontextmanager
|
|
8
7
|
from datetime import UTC, datetime
|
|
9
8
|
from types import FrameType
|
|
10
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
11
10
|
from urllib.parse import parse_qs
|
|
12
11
|
|
|
13
12
|
import aio_pika
|
|
@@ -36,7 +35,7 @@ from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
|
36
35
|
logger = logging.getLogger(__name__)
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
def
|
|
38
|
+
def _extract_scheduled_actions(
|
|
40
39
|
app: Microservice, container: Container, scheduler_names: set[str] | None = None
|
|
41
40
|
) -> list[ScheduledActionData]:
|
|
42
41
|
scheduled_actions: list[ScheduledActionData] = []
|
|
@@ -64,7 +63,7 @@ def extract_scheduled_actions(
|
|
|
64
63
|
# region Message Broker Dispatcher
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
class
|
|
66
|
+
class _MessageBrokerDispatcher(ABC):
|
|
68
67
|
|
|
69
68
|
@abstractmethod
|
|
70
69
|
async def dispatch_scheduled_action(
|
|
@@ -100,7 +99,7 @@ class MessageBrokerDispatcher(ABC):
|
|
|
100
99
|
pass
|
|
101
100
|
|
|
102
101
|
|
|
103
|
-
class
|
|
102
|
+
class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
|
|
104
103
|
|
|
105
104
|
def __init__(self, url: str) -> None:
|
|
106
105
|
self.url = url
|
|
@@ -206,13 +205,13 @@ class RabbitMQBrokerDispatcher(MessageBrokerDispatcher):
|
|
|
206
205
|
await self.conn_pool.close()
|
|
207
206
|
|
|
208
207
|
|
|
209
|
-
def
|
|
208
|
+
def _get_message_broker_dispatcher_from_url(url: str) -> _MessageBrokerDispatcher:
|
|
210
209
|
"""
|
|
211
210
|
Factory function to create a message broker instance from a URL.
|
|
212
211
|
Currently, only RabbitMQ is supported.
|
|
213
212
|
"""
|
|
214
213
|
if url.startswith("amqp://") or url.startswith("amqps://"):
|
|
215
|
-
return
|
|
214
|
+
return _RabbitMQBrokerDispatcher(url=url)
|
|
216
215
|
else:
|
|
217
216
|
raise ValueError(f"Unsupported message broker URL: {url}")
|
|
218
217
|
|
|
@@ -220,7 +219,7 @@ def get_message_broker_dispatcher_from_url(url: str) -> MessageBrokerDispatcher:
|
|
|
220
219
|
# endregion
|
|
221
220
|
|
|
222
221
|
|
|
223
|
-
class
|
|
222
|
+
class BeatWorker:
|
|
224
223
|
|
|
225
224
|
def __init__(
|
|
226
225
|
self,
|
|
@@ -228,11 +227,11 @@ class SchedulerV2:
|
|
|
228
227
|
interval: int,
|
|
229
228
|
broker_url: str,
|
|
230
229
|
backend_url: str,
|
|
231
|
-
|
|
230
|
+
scheduled_action_names: set[str] | None = None,
|
|
232
231
|
) -> None:
|
|
233
232
|
self.app = app
|
|
234
233
|
|
|
235
|
-
self.broker:
|
|
234
|
+
self.broker: _MessageBrokerDispatcher = _get_message_broker_dispatcher_from_url(
|
|
236
235
|
broker_url
|
|
237
236
|
)
|
|
238
237
|
self.backend: MessageBrokerBackend = get_message_broker_backend_from_url(
|
|
@@ -240,7 +239,7 @@ class SchedulerV2:
|
|
|
240
239
|
)
|
|
241
240
|
|
|
242
241
|
self.interval = interval
|
|
243
|
-
self.scheduler_names =
|
|
242
|
+
self.scheduler_names = scheduled_action_names
|
|
244
243
|
self.container = Container(self.app)
|
|
245
244
|
self.uow_provider = UnitOfWorkContextProvider(app, self.container)
|
|
246
245
|
|
|
@@ -266,7 +265,7 @@ class SchedulerV2:
|
|
|
266
265
|
"""
|
|
267
266
|
async with self.lifecycle():
|
|
268
267
|
|
|
269
|
-
scheduled_actions =
|
|
268
|
+
scheduled_actions = _extract_scheduled_actions(
|
|
270
269
|
self.app, self.container, self.scheduler_names
|
|
271
270
|
)
|
|
272
271
|
|
|
@@ -341,14 +340,3 @@ class SchedulerV2:
|
|
|
341
340
|
|
|
342
341
|
await self.backend.dispose()
|
|
343
342
|
await self.broker.dispose()
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
@asynccontextmanager
|
|
347
|
-
async def none_context() -> AsyncGenerator[None, None]:
|
|
348
|
-
yield
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
logging.basicConfig(
|
|
352
|
-
level=logging.INFO,
|
|
353
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
354
|
-
)
|
jararaca/utils/rabbitmq_utils.py
CHANGED
|
@@ -332,11 +332,16 @@ class RabbitmqUtils:
|
|
|
332
332
|
) -> AbstractQueue:
|
|
333
333
|
"""
|
|
334
334
|
Declare a scheduler queue with simple durable configuration.
|
|
335
|
+
The queue has a max length of 1 to ensure only one scheduled task
|
|
336
|
+
is processed at a time.
|
|
335
337
|
"""
|
|
336
338
|
return await channel.declare_queue(
|
|
337
339
|
name=queue_name,
|
|
338
340
|
durable=True,
|
|
339
341
|
passive=passive,
|
|
342
|
+
arguments={
|
|
343
|
+
"x-max-length": 1,
|
|
344
|
+
},
|
|
340
345
|
)
|
|
341
346
|
|
|
342
347
|
@classmethod
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
jararaca/__init__.py,sha256=
|
|
1
|
+
jararaca/__init__.py,sha256=EdOPigKYraoG7I-Hl9V3XteqrbhdBS3xy1rUakh58lc,17949
|
|
2
2
|
jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
|
|
3
3
|
jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
|
|
4
4
|
jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
|
|
5
5
|
jararaca/broker_backend/redis_broker_backend.py,sha256=a7DHchy3NAiD71Ix8SwmQOUnniu7uup-Woa4ON_4J7I,5786
|
|
6
|
-
jararaca/cli.py,sha256=
|
|
6
|
+
jararaca/cli.py,sha256=OkeemUnq3_Zk8XaMerzTct3La1NLVhZdQd-N-3lEP2c,19253
|
|
7
7
|
jararaca/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
jararaca/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
jararaca/core/providers.py,sha256=wktH84FK7c1s2wNq-fudf1uMfi3CQBR0neU2czJ_L0U,434
|
|
@@ -20,8 +20,7 @@ jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py,sha256=_DEHwIH
|
|
|
20
20
|
jararaca/messagebus/interceptors/publisher_interceptor.py,sha256=ojy1bRhqMgrkQljcGGS8cd8-8pUjL8ZHjIUkdmaAnNM,1325
|
|
21
21
|
jararaca/messagebus/message.py,sha256=U6cyd2XknX8mtm0333slz5fanky2PFLWCmokAO56vvU,819
|
|
22
22
|
jararaca/messagebus/publisher.py,sha256=JTkxdKbvxvDWT8nK8PVEyyX061vYYbKQMxRHXrZtcEY,2173
|
|
23
|
-
jararaca/messagebus/worker.py,sha256=
|
|
24
|
-
jararaca/messagebus/worker_v2.py,sha256=18oD-6Ip_rOa90p53EcEPlvkXho3SWrC40l4OVSIsE4,22356
|
|
23
|
+
jararaca/messagebus/worker.py,sha256=18oD-6Ip_rOa90p53EcEPlvkXho3SWrC40l4OVSIsE4,22356
|
|
25
24
|
jararaca/microservice.py,sha256=rRIimfeP2-wf289PKoUbk9wrSdA0ga_qWz5JNgQ5IE0,9667
|
|
26
25
|
jararaca/observability/decorators.py,sha256=MOIr2PttPYYvRwEdfQZEwD5RxKHOTv8UEy9n1YQVoKw,2281
|
|
27
26
|
jararaca/observability/interceptor.py,sha256=U4ZLM0f8j6Q7gMUKKnA85bnvD-Qa0ii79Qa_X8KsXAQ,1498
|
|
@@ -58,18 +57,17 @@ jararaca/rpc/http/backends/otel.py,sha256=Uc6CjHSCZ5hvnK1fNFv3ota5xzUFnvIl1JOpG3
|
|
|
58
57
|
jararaca/rpc/http/decorators.py,sha256=oUSzgMGI8w6SoKiz3GltDbd3BWAuyY60F23cdRRNeiw,11897
|
|
59
58
|
jararaca/rpc/http/httpx.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
59
|
jararaca/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
|
+
jararaca/scheduler/beat_worker.py,sha256=7uSr4sth9jy8ppWiMNgV-pnGSYI0xdbdf8WJuBcgJVY,11598
|
|
61
61
|
jararaca/scheduler/decorators.py,sha256=iyWFvPLCRh9c0YReQRemI2mLuaUv7r929So-xuKIWUs,4605
|
|
62
|
-
jararaca/scheduler/scheduler.py,sha256=1T7qKIOiU9WD2p0aJsiUW-MOArJZyUSMwpv_14ziTQM,6188
|
|
63
|
-
jararaca/scheduler/scheduler_v2.py,sha256=zrpbiYEzEn7dugbH-MhodOBVjPRzpJ9Z3iEPg8qZVB4,11839
|
|
64
62
|
jararaca/scheduler/types.py,sha256=4HEQOmVIDp-BYLSzqmqSFIio1bd51WFmgFPIzPpVu04,135
|
|
65
63
|
jararaca/tools/app_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
64
|
jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaNXMp8fd1Ndk,941
|
|
67
65
|
jararaca/tools/app_config/interceptor.py,sha256=HV8h4AxqUc_ACs5do4BSVlyxlRXzx7HqJtoVO9tfRnQ,2611
|
|
68
66
|
jararaca/tools/typescript/interface_parser.py,sha256=35xbOrZDQDyTXdMrVZQ8nnFw79f28lJuLYNHAspIqi8,30492
|
|
69
67
|
jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
|
-
jararaca/utils/rabbitmq_utils.py,sha256=
|
|
71
|
-
jararaca-0.3.
|
|
72
|
-
jararaca-0.3.
|
|
73
|
-
jararaca-0.3.
|
|
74
|
-
jararaca-0.3.
|
|
75
|
-
jararaca-0.3.
|
|
68
|
+
jararaca/utils/rabbitmq_utils.py,sha256=LxGc4_AsjDrwzRFeiKzf0O8fgzMTq47uWvxzXqJZJ4o,12401
|
|
69
|
+
jararaca-0.3.11a11.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
70
|
+
jararaca-0.3.11a11.dist-info/METADATA,sha256=Bw1zSZApDCkGCTrM-uj3BjAB2g21k_ii04Q4bsik7SM,4998
|
|
71
|
+
jararaca-0.3.11a11.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
72
|
+
jararaca-0.3.11a11.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
|
|
73
|
+
jararaca-0.3.11a11.dist-info/RECORD,,
|