jararaca 0.4.0a5__py3-none-any.whl → 0.4.0a19__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.
- jararaca/__init__.py +9 -9
- jararaca/cli.py +643 -4
- jararaca/core/providers.py +4 -0
- jararaca/helpers/__init__.py +3 -0
- jararaca/helpers/global_scheduler/__init__.py +3 -0
- jararaca/helpers/global_scheduler/config.py +21 -0
- jararaca/helpers/global_scheduler/controller.py +42 -0
- jararaca/helpers/global_scheduler/registry.py +32 -0
- jararaca/messagebus/decorators.py +104 -10
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +50 -8
- jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
- jararaca/messagebus/interceptors/publisher_interceptor.py +25 -3
- jararaca/messagebus/worker.py +276 -200
- jararaca/microservice.py +3 -1
- jararaca/observability/providers/otel.py +31 -13
- jararaca/persistence/base.py +1 -1
- jararaca/persistence/utilities.py +47 -24
- jararaca/presentation/decorators.py +3 -3
- jararaca/reflect/decorators.py +24 -10
- jararaca/reflect/helpers.py +18 -0
- jararaca/rpc/http/__init__.py +2 -2
- jararaca/rpc/http/decorators.py +9 -9
- jararaca/scheduler/beat_worker.py +14 -14
- jararaca/tools/typescript/decorators.py +4 -4
- jararaca/tools/typescript/interface_parser.py +3 -1
- jararaca/utils/env_parse_utils.py +133 -0
- jararaca/utils/rabbitmq_utils.py +47 -0
- jararaca/utils/retry.py +11 -13
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +2 -1
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/RECORD +35 -27
- pyproject.toml +2 -1
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/LICENSE +0 -0
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/LICENSES/GPL-3.0-or-later.txt +0 -0
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +0 -0
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from jararaca.helpers.global_scheduler.config import GlobalSchedulerConfigAnnotated
|
|
8
|
+
from jararaca.helpers.global_scheduler.registry import (
|
|
9
|
+
GlobalScheduleFnType,
|
|
10
|
+
GlobalSchedulerRegistry,
|
|
11
|
+
)
|
|
12
|
+
from jararaca.messagebus.decorators import MessageBusController
|
|
13
|
+
from jararaca.scheduler.decorators import ScheduledAction
|
|
14
|
+
from jararaca.utils.env_parse_utils import get_env_str
|
|
15
|
+
|
|
16
|
+
SCHEDULER_CRON = get_env_str("SCHEDULER_CRON", "*/5 * * * *")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@MessageBusController()
|
|
20
|
+
class GlobalSchedulerController:
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: GlobalSchedulerConfigAnnotated):
|
|
23
|
+
self._config = config
|
|
24
|
+
|
|
25
|
+
@ScheduledAction(cron=SCHEDULER_CRON)
|
|
26
|
+
async def trigger_scheduled_actions(self) -> None:
|
|
27
|
+
"""Trigger all registered scheduled actions."""
|
|
28
|
+
|
|
29
|
+
taks = []
|
|
30
|
+
|
|
31
|
+
semaphore = asyncio.Semaphore(self._config.MAX_CONCURRENT_JOBS)
|
|
32
|
+
|
|
33
|
+
for action in GlobalSchedulerRegistry.get_registered_actions():
|
|
34
|
+
task = asyncio.create_task(self._run_with_semaphore(semaphore, action))
|
|
35
|
+
|
|
36
|
+
taks.append(task)
|
|
37
|
+
|
|
38
|
+
async def _run_with_semaphore(
|
|
39
|
+
self, semaphore: asyncio.Semaphore, action: GlobalScheduleFnType
|
|
40
|
+
) -> None:
|
|
41
|
+
async with semaphore:
|
|
42
|
+
await action()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
GlobalScheduleFnType = Callable[[], Awaitable[Any]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobalSchedulerRegistry:
|
|
12
|
+
"""Registry for the Global Scheduler helper."""
|
|
13
|
+
|
|
14
|
+
_REGISTRY: list[GlobalScheduleFnType] = []
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def register(cls, fn: GlobalScheduleFnType) -> None:
|
|
18
|
+
"""Register a scheduled action function.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
fn: The scheduled action function to register.
|
|
22
|
+
"""
|
|
23
|
+
cls._REGISTRY.append(fn)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get_registered_actions(cls) -> list[GlobalScheduleFnType]:
|
|
27
|
+
"""Get the list of registered scheduled action functions.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A list of registered scheduled action functions.
|
|
31
|
+
"""
|
|
32
|
+
return cls._REGISTRY
|
|
@@ -5,35 +5,129 @@
|
|
|
5
5
|
|
|
6
6
|
import inspect
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Any, Awaitable, Callable
|
|
8
|
+
from typing import Any, Awaitable, Callable, Literal, TypeVar, cast, get_args
|
|
9
9
|
|
|
10
|
-
from jararaca.messagebus.message import INHERITS_MESSAGE_CO
|
|
10
|
+
from jararaca.messagebus.message import INHERITS_MESSAGE_CO, Message, MessageOf
|
|
11
11
|
from jararaca.reflect.controller_inspect import (
|
|
12
12
|
ControllerMemberReflect,
|
|
13
13
|
inspect_controller,
|
|
14
14
|
)
|
|
15
|
-
from jararaca.reflect.decorators import
|
|
15
|
+
from jararaca.reflect.decorators import (
|
|
16
|
+
FUNC_OR_TYPE_T,
|
|
17
|
+
GenericStackableDecorator,
|
|
18
|
+
StackableDecorator,
|
|
19
|
+
)
|
|
20
|
+
from jararaca.reflect.helpers import is_generic_alias
|
|
16
21
|
from jararaca.scheduler.decorators import ScheduledAction, ScheduledActionData
|
|
22
|
+
from jararaca.utils.env_parse_utils import get_env_float, get_env_int, is_env_truffy
|
|
23
|
+
from jararaca.utils.retry import RetryPolicy
|
|
24
|
+
|
|
25
|
+
AcceptableHandler = (
|
|
26
|
+
Callable[[Any, MessageOf[Any]], Awaitable[None]]
|
|
27
|
+
| Callable[[Any, Any], Awaitable[None]]
|
|
28
|
+
)
|
|
29
|
+
MessageHandlerT = TypeVar("MessageHandlerT", bound=AcceptableHandler)
|
|
17
30
|
|
|
31
|
+
DEFAULT_TIMEOUT = get_env_int("JARARACA_MESSAGEBUS_HANDLER_TIMEOUT")
|
|
32
|
+
DEFAULT_NACK_ON_EXCEPTION = is_env_truffy("JARARACA_MESSAGEBUS_NACK_ON_EXCEPTION")
|
|
33
|
+
DEFAULT_AUTO_ACK = is_env_truffy("JARARACA_MESSAGEBUS_AUTO_ACK")
|
|
34
|
+
DEFAULT_NACK_DELAY_ON_EXCEPTION = get_env_float(
|
|
35
|
+
"JARARACA_MESSAGEBUS_NACK_DELAY_ON_EXCEPTION"
|
|
36
|
+
)
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
|
|
39
|
+
class MessageHandler(GenericStackableDecorator[AcceptableHandler]):
|
|
20
40
|
|
|
21
41
|
def __init__(
|
|
22
42
|
self,
|
|
23
43
|
message: type[INHERITS_MESSAGE_CO],
|
|
24
|
-
|
|
44
|
+
*,
|
|
45
|
+
timeout: int | None = DEFAULT_TIMEOUT if DEFAULT_TIMEOUT is not False else None,
|
|
25
46
|
exception_handler: Callable[[BaseException], None] | None = None,
|
|
26
|
-
nack_on_exception: bool =
|
|
27
|
-
|
|
47
|
+
nack_on_exception: bool = DEFAULT_NACK_ON_EXCEPTION,
|
|
48
|
+
nack_delay_on_exception: float = DEFAULT_NACK_DELAY_ON_EXCEPTION or 5.0,
|
|
49
|
+
auto_ack: bool = DEFAULT_AUTO_ACK,
|
|
28
50
|
name: str | None = None,
|
|
51
|
+
retry_config: RetryPolicy | None = None,
|
|
29
52
|
) -> None:
|
|
30
53
|
self.message_type = message
|
|
31
54
|
|
|
32
55
|
self.timeout = timeout
|
|
33
56
|
self.exception_handler = exception_handler
|
|
34
57
|
self.nack_on_exception = nack_on_exception
|
|
58
|
+
self.nack_delay_on_exception = nack_delay_on_exception
|
|
59
|
+
|
|
35
60
|
self.auto_ack = auto_ack
|
|
36
61
|
self.name = name
|
|
62
|
+
self.retry_config = retry_config
|
|
63
|
+
|
|
64
|
+
def __call__(self, subject: MessageHandlerT) -> MessageHandlerT:
|
|
65
|
+
return cast(MessageHandlerT, super().__call__(subject))
|
|
66
|
+
|
|
67
|
+
def pre_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
68
|
+
MessageHandler.validate_decorated_fn(subject)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def validate_decorated_fn(
|
|
72
|
+
subject: FUNC_OR_TYPE_T,
|
|
73
|
+
) -> tuple[Literal["WRAPPED", "DIRECT"], type[Message]]:
|
|
74
|
+
"""Validates that the decorated function has the correct signature
|
|
75
|
+
the decorated must follow one of the patterns:
|
|
76
|
+
|
|
77
|
+
async def handler(self, message: MessageOf[YourMessageType]) -> None:
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
async def handler(self, message: YourMessageType) -> None:
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if not inspect.iscoroutinefunction(subject):
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
"Message handler '%s' must be a coroutine function"
|
|
88
|
+
% (subject.__qualname__)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
signature = inspect.signature(subject)
|
|
92
|
+
|
|
93
|
+
parameters = list(signature.parameters.values())
|
|
94
|
+
|
|
95
|
+
if len(parameters) != 2:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"Message handler '%s' must have exactly two parameters (self, message)"
|
|
98
|
+
% (subject.__qualname__)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
message_param = parameters[1]
|
|
102
|
+
|
|
103
|
+
if message_param.annotation is inspect.Parameter.empty:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
"Message handler '%s' must have type annotation for the message parameter"
|
|
106
|
+
% (subject.__qualname__)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
annotation_type = message_param.annotation
|
|
110
|
+
mode: Literal["WRAPPED", "DIRECT"]
|
|
111
|
+
if is_generic_alias(annotation_type):
|
|
112
|
+
|
|
113
|
+
message_model_type = get_args(annotation_type)[0]
|
|
114
|
+
|
|
115
|
+
mode = "WRAPPED"
|
|
116
|
+
|
|
117
|
+
else:
|
|
118
|
+
message_model_type = annotation_type
|
|
119
|
+
|
|
120
|
+
mode = "DIRECT"
|
|
121
|
+
|
|
122
|
+
if not inspect.isclass(message_model_type) or not issubclass(
|
|
123
|
+
message_model_type, Message
|
|
124
|
+
):
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"Message handler '%s' message parameter must be of type 'MessageOf[YourMessageType]' or 'YourMessageType' where 'YourMessageType' is a subclass of 'Message'"
|
|
127
|
+
% (subject.__qualname__)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return mode, message_model_type
|
|
37
131
|
|
|
38
132
|
|
|
39
133
|
@dataclass(frozen=True)
|
|
@@ -73,16 +167,16 @@ class MessageBusController(StackableDecorator):
|
|
|
73
167
|
def get_messagebus_factory(
|
|
74
168
|
self,
|
|
75
169
|
) -> Callable[
|
|
76
|
-
[
|
|
170
|
+
[FUNC_OR_TYPE_T], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]
|
|
77
171
|
]:
|
|
78
172
|
if self.messagebus_factory is None:
|
|
79
173
|
raise Exception("MessageBus factory is not set")
|
|
80
174
|
return self.messagebus_factory
|
|
81
175
|
|
|
82
|
-
def post_decorated(self, subject:
|
|
176
|
+
def post_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
83
177
|
|
|
84
178
|
def messagebus_factory(
|
|
85
|
-
instance:
|
|
179
|
+
instance: FUNC_OR_TYPE_T,
|
|
86
180
|
) -> tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]:
|
|
87
181
|
handlers: MESSAGE_HANDLER_DATA_SET = set()
|
|
88
182
|
|
|
@@ -9,6 +9,7 @@ from datetime import tzinfo as _TzInfo
|
|
|
9
9
|
from typing import Any, AsyncGenerator
|
|
10
10
|
|
|
11
11
|
import aio_pika
|
|
12
|
+
import tenacity
|
|
12
13
|
from aio_pika.abc import AbstractConnection
|
|
13
14
|
from pydantic import BaseModel
|
|
14
15
|
|
|
@@ -71,7 +72,9 @@ class AIOPikaMessagePublisher(MessagePublisher):
|
|
|
71
72
|
)
|
|
72
73
|
)
|
|
73
74
|
|
|
74
|
-
async def schedule(
|
|
75
|
+
async def schedule(
|
|
76
|
+
self, message: IMessage, when: datetime, timezone: _TzInfo
|
|
77
|
+
) -> None:
|
|
75
78
|
if not self.message_broker_backend:
|
|
76
79
|
raise NotImplementedError(
|
|
77
80
|
"Schedule is not implemented for AIOPikaMessagePublisher"
|
|
@@ -110,6 +113,18 @@ class GenericPoolConfig(BaseModel):
|
|
|
110
113
|
max_size: int
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
default_retry = tenacity.retry(
|
|
117
|
+
wait=tenacity.wait_exponential_jitter(initial=1, max=60),
|
|
118
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
default_retry_channel = tenacity.retry(
|
|
122
|
+
wait=tenacity.wait_exponential_jitter(initial=1, max=60),
|
|
123
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
124
|
+
stop=tenacity.stop_after_attempt(5),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
113
128
|
class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
114
129
|
|
|
115
130
|
def __init__(
|
|
@@ -130,6 +145,7 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
130
145
|
|
|
131
146
|
if connection_pool_config:
|
|
132
147
|
|
|
148
|
+
@default_retry
|
|
133
149
|
async def get_connection() -> AbstractConnection:
|
|
134
150
|
return await aio_pika.connect(self.url)
|
|
135
151
|
|
|
@@ -140,6 +156,7 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
140
156
|
|
|
141
157
|
if channel_pool_config:
|
|
142
158
|
|
|
159
|
+
@default_retry_channel
|
|
143
160
|
async def get_channel() -> aio_pika.abc.AbstractChannel:
|
|
144
161
|
async with self.acquire_connection() as connection:
|
|
145
162
|
return await connection.channel(publisher_confirms=False)
|
|
@@ -148,10 +165,14 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
148
165
|
get_channel, max_size=channel_pool_config.max_size
|
|
149
166
|
)
|
|
150
167
|
|
|
168
|
+
@default_retry
|
|
169
|
+
async def _connect(self) -> AbstractConnection:
|
|
170
|
+
return await aio_pika.connect(self.url)
|
|
171
|
+
|
|
151
172
|
@asynccontextmanager
|
|
152
173
|
async def acquire_connection(self) -> AsyncGenerator[AbstractConnection, Any]:
|
|
153
174
|
if not self.connection_pool:
|
|
154
|
-
async with await
|
|
175
|
+
async with await self._connect() as connection:
|
|
155
176
|
yield connection
|
|
156
177
|
else:
|
|
157
178
|
|
|
@@ -162,12 +183,33 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
162
183
|
async def acquire_channel(
|
|
163
184
|
self,
|
|
164
185
|
) -> AsyncGenerator[aio_pika.abc.AbstractChannel, Any]:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
|
|
187
|
+
async for attempt in tenacity.AsyncRetrying(
|
|
188
|
+
wait=tenacity.wait_exponential_jitter(initial=1, max=10),
|
|
189
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
190
|
+
# stop=tenacity.stop_after_attempt(9000),
|
|
191
|
+
):
|
|
192
|
+
with attempt:
|
|
193
|
+
|
|
194
|
+
if not self.connection_pool or not self.channel_pool:
|
|
195
|
+
async with self.acquire_connection() as connection:
|
|
196
|
+
yield await connection.channel(publisher_confirms=False)
|
|
197
|
+
else:
|
|
198
|
+
|
|
199
|
+
async with self.connection_pool.acquire() as connection:
|
|
200
|
+
if not connection.connected.is_set():
|
|
201
|
+
await connection.connect()
|
|
202
|
+
|
|
203
|
+
async with connection.channel(
|
|
204
|
+
publisher_confirms=False
|
|
205
|
+
) as channel:
|
|
206
|
+
yield channel
|
|
207
|
+
# await connection.close()
|
|
208
|
+
if attempt.retry_state.attempt_number > 1:
|
|
209
|
+
logger.warning(
|
|
210
|
+
"Later successful connection attempt #%d",
|
|
211
|
+
attempt.retry_state.attempt_number,
|
|
212
|
+
)
|
|
171
213
|
|
|
172
214
|
@asynccontextmanager
|
|
173
215
|
async def provide_connection(self) -> AsyncGenerator[AIOPikaMessagePublisher, Any]:
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime, timedelta, tzinfo
|
|
7
|
+
|
|
8
|
+
from jararaca.messagebus.publisher import IMessage, MessagePublisher
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CollectorDelayedMessageData:
|
|
13
|
+
message: IMessage
|
|
14
|
+
when: datetime
|
|
15
|
+
timezone: tzinfo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MessagePublisherCollector(MessagePublisher):
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.staged_delayed_messages: list[CollectorDelayedMessageData] = []
|
|
22
|
+
self.staged_messages: list[IMessage] = []
|
|
23
|
+
|
|
24
|
+
async def publish(self, message: IMessage, topic: str) -> None:
|
|
25
|
+
self.staged_messages.append(message)
|
|
26
|
+
|
|
27
|
+
async def delay(self, message: IMessage, seconds: int) -> None:
|
|
28
|
+
self.staged_delayed_messages.append(
|
|
29
|
+
CollectorDelayedMessageData(
|
|
30
|
+
message=message,
|
|
31
|
+
when=datetime.now(UTC) + timedelta(seconds=seconds),
|
|
32
|
+
timezone=UTC,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def schedule(
|
|
37
|
+
self, message: IMessage, when: datetime, timezone: tzinfo
|
|
38
|
+
) -> None:
|
|
39
|
+
self.staged_delayed_messages.append(
|
|
40
|
+
CollectorDelayedMessageData(
|
|
41
|
+
message=message,
|
|
42
|
+
when=when,
|
|
43
|
+
timezone=timezone,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def fill(self, publisher: MessagePublisher) -> None:
|
|
48
|
+
for message in self.staged_messages:
|
|
49
|
+
await publisher.publish(message, message.MESSAGE_TOPIC)
|
|
50
|
+
|
|
51
|
+
for delayed_message in self.staged_delayed_messages:
|
|
52
|
+
await publisher.schedule(
|
|
53
|
+
delayed_message.message,
|
|
54
|
+
delayed_message.when,
|
|
55
|
+
delayed_message.timezone,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def has_messages(self) -> bool:
|
|
59
|
+
return bool(self.staged_messages or self.staged_delayed_messages)
|
|
60
|
+
|
|
61
|
+
async def flush(self) -> None:
|
|
62
|
+
raise NotImplementedError("I'm just a poor little collector! :(")
|
|
@@ -6,6 +6,9 @@ from contextlib import asynccontextmanager
|
|
|
6
6
|
from typing import AsyncContextManager, AsyncGenerator, Protocol
|
|
7
7
|
|
|
8
8
|
from jararaca.broker_backend import MessageBrokerBackend
|
|
9
|
+
from jararaca.messagebus.interceptors.message_publisher_collector import (
|
|
10
|
+
MessagePublisherCollector,
|
|
11
|
+
)
|
|
9
12
|
from jararaca.messagebus.publisher import MessagePublisher, provide_message_publisher
|
|
10
13
|
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
11
14
|
|
|
@@ -22,10 +25,15 @@ class MessageBusPublisherInterceptor(AppInterceptor):
|
|
|
22
25
|
connection_factory: MessageBusConnectionFactory,
|
|
23
26
|
connection_name: str = "default",
|
|
24
27
|
message_scheduler: MessageBrokerBackend | None = None,
|
|
28
|
+
*,
|
|
29
|
+
open_connection_at_end_of_transaction: bool = False,
|
|
25
30
|
):
|
|
26
31
|
self.connection_factory = connection_factory
|
|
27
32
|
self.connection_name = connection_name
|
|
28
33
|
self.message_scheduler = message_scheduler
|
|
34
|
+
self.open_connection_at_end_of_transaction = (
|
|
35
|
+
open_connection_at_end_of_transaction
|
|
36
|
+
)
|
|
29
37
|
|
|
30
38
|
@asynccontextmanager
|
|
31
39
|
async def intercept(
|
|
@@ -35,8 +43,22 @@ class MessageBusPublisherInterceptor(AppInterceptor):
|
|
|
35
43
|
yield
|
|
36
44
|
return
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
if self.open_connection_at_end_of_transaction:
|
|
47
|
+
|
|
48
|
+
collector = MessagePublisherCollector()
|
|
49
|
+
with provide_message_publisher(self.connection_name, collector):
|
|
40
50
|
yield
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
if collector.has_messages():
|
|
53
|
+
async with self.connection_factory.provide_connection() as connection:
|
|
54
|
+
await collector.fill(connection)
|
|
55
|
+
await connection.flush()
|
|
56
|
+
return
|
|
57
|
+
else:
|
|
58
|
+
|
|
59
|
+
yield
|
|
60
|
+
|
|
61
|
+
async with self.connection_factory.provide_connection() as connection:
|
|
62
|
+
with provide_message_publisher(self.connection_name, connection):
|
|
63
|
+
|
|
64
|
+
await connection.flush()
|