jararaca 0.3.11a10__py3-none-any.whl → 0.3.11a12__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.

@@ -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 AioPikaMicroserviceConsumer:
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 DL kit
129
+ # Get existing exchange and queues
104
130
  try:
105
- main_ex = await RabbitmqUtils.get_main_exchange(
131
+ exchange = await RabbitmqUtils.get_main_exchange(
106
132
  channel=channel,
107
133
  exchange_name=self.config.exchange,
108
134
  )
109
- dlx, dlq = await RabbitmqUtils.get_dl_kit(channel=channel)
110
- except Exception as e:
111
- logger.error(
112
- f"Required exchange or queue infrastructure not found. "
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.get_worker_v1_queue(
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 Exception as e:
157
+ except (ChannelNotFoundEntity, ChannelClosed, AMQPError) as e:
131
158
  logger.error(
132
- f"Worker queue '{queue_name}' not found. "
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.consumer.incoming_map.get(routing_key)
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__(self, app: Microservice, config: AioPikaWorkerConfig) -> None:
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.config = config
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: AioPikaMicroserviceConsumer | None = None
547
+ self._consumer: MessageBusConsumer | None = None
338
548
 
339
549
  @property
340
- def consumer(self) -> AioPikaMicroserviceConsumer:
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, handler_names: set[str] | None = None) -> None:
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 instance_type in self.app.controllers:
349
- controller = MessageBusController.get_messagebus(instance_type)
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(instance_type)
565
+ instance: Any = self.container.get_by_type(instance_class)
355
566
 
356
567
  factory = controller.get_messagebus_factory()
357
- handlers, _ = factory(instance)
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 handler_names is not None and handler_data.spec.name is not None:
367
- if handler_data.spec.name not in handler_names:
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 handler_names is not None and handler_data.spec.name is None:
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
- consumer = self._consumer = AioPikaMicroserviceConsumer(
386
- config=self.config,
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, handler_names: set[str] | None = None) -> None:
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.shutdown_event.set()
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(handler_names=handler_names))
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, AsyncGenerator
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 extract_scheduled_actions(
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 MessageBrokerDispatcher(ABC):
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 RabbitMQBrokerDispatcher(MessageBrokerDispatcher):
102
+ class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
104
103
 
105
104
  def __init__(self, url: str) -> None:
106
105
  self.url = url
@@ -196,7 +195,7 @@ class RabbitMQBrokerDispatcher(MessageBrokerDispatcher):
196
195
  queue_name = ScheduledAction.get_function_id(sched_act_data.callable)
197
196
 
198
197
  # Try to get existing queue
199
- await RabbitmqUtils.get_scheduler_queue(
198
+ await RabbitmqUtils.get_scheduled_action_queue(
200
199
  channel=channel,
201
200
  queue_name=queue_name,
202
201
  )
@@ -206,13 +205,13 @@ class RabbitMQBrokerDispatcher(MessageBrokerDispatcher):
206
205
  await self.conn_pool.close()
207
206
 
208
207
 
209
- def get_message_broker_dispatcher_from_url(url: str) -> MessageBrokerDispatcher:
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 RabbitMQBrokerDispatcher(url=url)
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 SchedulerV2:
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
- scheduler_names: set[str] | None = None,
230
+ scheduled_action_names: set[str] | None = None,
232
231
  ) -> None:
233
232
  self.app = app
234
233
 
235
- self.broker: MessageBrokerDispatcher = get_message_broker_dispatcher_from_url(
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 = 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 = extract_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
- )
@@ -222,7 +222,7 @@ class RabbitmqUtils:
222
222
  raise
223
223
 
224
224
  @classmethod
225
- async def declare_queue(
225
+ async def declare_worker_queue(
226
226
  cls,
227
227
  channel: AbstractChannel,
228
228
  queue_name: str,
@@ -243,64 +243,13 @@ class RabbitmqUtils:
243
243
  )
244
244
 
245
245
  @classmethod
246
- async def get_worker_v1_queue(
246
+ async def get_scheduled_action_queue(
247
247
  cls,
248
248
  channel: AbstractChannel,
249
249
  queue_name: str,
250
250
  ) -> AbstractQueue:
251
251
  """
252
- Get a worker v1 queue.
253
- """
254
- try:
255
- return await channel.get_queue(queue_name)
256
- except ChannelNotFoundEntity as e:
257
- logger.error(
258
- f"Worker queue '{queue_name}' does not exist. "
259
- f"Please use the declare command to create it first. Error: {e}"
260
- )
261
- raise
262
- except ChannelClosed as e:
263
- logger.error(
264
- f"Channel closed while getting worker queue '{queue_name}'. "
265
- f"Error: {e}"
266
- )
267
- raise
268
- except AMQPError as e:
269
- logger.error(
270
- f"AMQP error while getting worker queue '{queue_name}'. " f"Error: {e}"
271
- )
272
- raise
273
-
274
- @classmethod
275
- async def declare_worker_v1_queue(
276
- cls,
277
- channel: AbstractChannel,
278
- queue_name: str,
279
- dlx_name: str,
280
- dlq_name: str,
281
- passive: bool = False,
282
- ) -> AbstractQueue:
283
- """
284
- Declare a worker v1 queue with custom dead letter exchange and routing key.
285
- """
286
- return await channel.declare_queue(
287
- passive=passive,
288
- name=queue_name,
289
- arguments={
290
- "x-dead-letter-exchange": dlx_name,
291
- "x-dead-letter-routing-key": dlq_name,
292
- },
293
- durable=True,
294
- )
295
-
296
- @classmethod
297
- async def get_scheduler_queue(
298
- cls,
299
- channel: AbstractChannel,
300
- queue_name: str,
301
- ) -> AbstractQueue:
302
- """
303
- Get a scheduler queue.
252
+ Get a scheduled action queue.
304
253
  """
305
254
  try:
306
255
  return await channel.get_queue(queue_name)
@@ -324,19 +273,24 @@ class RabbitmqUtils:
324
273
  raise
325
274
 
326
275
  @classmethod
327
- async def declare_scheduler_queue(
276
+ async def declare_scheduled_action_queue(
328
277
  cls,
329
278
  channel: AbstractChannel,
330
279
  queue_name: str,
331
280
  passive: bool = False,
332
281
  ) -> AbstractQueue:
333
282
  """
334
- Declare a scheduler queue with simple durable configuration.
283
+ Declare a scheduled action queue with simple durable configuration.
284
+ The queue has a max length of 1 to ensure only one scheduled task
285
+ is processed at a time.
335
286
  """
336
287
  return await channel.declare_queue(
337
288
  name=queue_name,
338
289
  durable=True,
339
290
  passive=passive,
291
+ arguments={
292
+ "x-max-length": 1,
293
+ },
340
294
  )
341
295
 
342
296
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: jararaca
3
- Version: 0.3.11a10
3
+ Version: 0.3.11a12
4
4
  Summary: A simple and fast API framework for Python
5
5
  Author: Lucas S
6
6
  Author-email: me@luscasleo.dev