jararaca 0.3.11a16__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.
- README.md +121 -0
- jararaca/__init__.py +189 -17
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +915 -51
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +8 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +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/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +121 -61
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
- jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
- jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +1002 -459
- jararaca/microservice.py +113 -2
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +225 -16
- jararaca/persistence/base.py +39 -3
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +73 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +252 -0
- jararaca/reflect/helpers.py +18 -0
- jararaca/reflect/metadata.py +34 -25
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +380 -115
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1077 -174
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/env_parse_utils.py +133 -0
- jararaca/utils/rabbitmq_utils.py +112 -39
- jararaca/utils/retry.py +19 -14
- jararaca-0.4.0a19.dist-info/LICENSE +674 -0
- jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
- jararaca-0.4.0a19.dist-info/RECORD +96 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
- pyproject.toml +132 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/entry_points.txt +0 -0
jararaca/utils/__init__.py
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Literal, Optional, TypeVar, overload
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_env_truffy(var_name: str) -> bool:
|
|
10
|
+
value = os.getenv(var_name, "").lower()
|
|
11
|
+
return value in ("1", "true", "yes", "on")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DF_BOOL_T = TypeVar("DF_BOOL_T", bound="bool")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def get_env_bool(
|
|
19
|
+
var_name: str, default: DF_BOOL_T
|
|
20
|
+
) -> DF_BOOL_T | bool | Literal["invalid"]: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def get_env_bool(
|
|
25
|
+
var_name: str, default: None = None
|
|
26
|
+
) -> bool | None | Literal["invalid"]: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_env_bool(
|
|
30
|
+
var_name: str, default: DF_BOOL_T | None = None
|
|
31
|
+
) -> DF_BOOL_T | bool | Literal["invalid"] | None:
|
|
32
|
+
value = os.getenv(var_name)
|
|
33
|
+
if value is None:
|
|
34
|
+
return default
|
|
35
|
+
value_lower = value.lower()
|
|
36
|
+
if value_lower in ("1", "true", "yes", "on"):
|
|
37
|
+
return True
|
|
38
|
+
elif value_lower in ("0", "false", "no", "off"):
|
|
39
|
+
return False
|
|
40
|
+
else:
|
|
41
|
+
return "invalid"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
DF_INT_T = TypeVar("DF_INT_T", bound="int | None | Literal[False]")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
def get_env_int(var_name: str, default: None = None) -> int | None | Literal[False]: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
def get_env_int(
|
|
53
|
+
var_name: str, default: DF_INT_T
|
|
54
|
+
) -> DF_INT_T | int | Literal[False]: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_env_int(
|
|
58
|
+
var_name: str, default: DF_INT_T = False # type: ignore[assignment]
|
|
59
|
+
) -> DF_INT_T | int | Literal[False]:
|
|
60
|
+
value = os.getenv(var_name)
|
|
61
|
+
if value is None:
|
|
62
|
+
return default
|
|
63
|
+
try:
|
|
64
|
+
return int(value)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
DF_FLOAT_T = TypeVar("DF_FLOAT_T", bound="float | None | Literal[False]")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@overload
|
|
73
|
+
def get_env_float(
|
|
74
|
+
var_name: str, default: None = None
|
|
75
|
+
) -> float | None | Literal[False]: ...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@overload
|
|
79
|
+
def get_env_float(
|
|
80
|
+
var_name: str, default: DF_FLOAT_T
|
|
81
|
+
) -> DF_FLOAT_T | float | Literal[False]: ...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_env_float(
|
|
85
|
+
var_name: str, default: DF_FLOAT_T = False # type: ignore[assignment]
|
|
86
|
+
) -> DF_FLOAT_T | float | Literal[False]:
|
|
87
|
+
value = os.getenv(var_name)
|
|
88
|
+
if value is None:
|
|
89
|
+
return default
|
|
90
|
+
try:
|
|
91
|
+
return float(value)
|
|
92
|
+
except ValueError:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
DF_STR_T = TypeVar("DF_STR_T", bound="Optional[str]")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@overload
|
|
100
|
+
def get_env_str(var_name: str, default: None = None) -> str | None: ...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def get_env_str(var_name: str, default: DF_STR_T) -> DF_STR_T | str: ...
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_env_str(var_name: str, default: DF_STR_T = None) -> DF_STR_T | str: # type: ignore[assignment]
|
|
108
|
+
value = os.getenv(var_name)
|
|
109
|
+
if value is None:
|
|
110
|
+
return default
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_env_list(var_name: str, separator: str = ",") -> list[str]:
|
|
115
|
+
value = os.getenv(var_name, "")
|
|
116
|
+
if not value:
|
|
117
|
+
return []
|
|
118
|
+
return [item.strip() for item in value.split(separator) if item.strip()]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_env_dict(
|
|
122
|
+
var_name: str, item_separator: str = ",", key_value_separator: str = "="
|
|
123
|
+
) -> dict[str, str]:
|
|
124
|
+
value = os.getenv(var_name, "")
|
|
125
|
+
result: dict[str, str] = {}
|
|
126
|
+
if not value:
|
|
127
|
+
return result
|
|
128
|
+
items = [item.strip() for item in value.split(item_separator) if item.strip()]
|
|
129
|
+
for item in items:
|
|
130
|
+
if key_value_separator in item:
|
|
131
|
+
key, val = item.split(key_value_separator, 1)
|
|
132
|
+
result[key.strip()] = val.strip()
|
|
133
|
+
return result
|
jararaca/utils/rabbitmq_utils.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import logging
|
|
2
6
|
|
|
3
7
|
from aio_pika.abc import AbstractChannel, AbstractExchange, AbstractQueue
|
|
@@ -27,20 +31,24 @@ class RabbitmqUtils:
|
|
|
27
31
|
)
|
|
28
32
|
except ChannelNotFoundEntity as e:
|
|
29
33
|
logger.error(
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
"Dead Letter Exchange '%s' does not exist. "
|
|
35
|
+
"Please use the declare command to create it first. Error: %s",
|
|
36
|
+
cls.DEAD_LETTER_EXCHANGE,
|
|
37
|
+
e,
|
|
32
38
|
)
|
|
33
39
|
raise
|
|
34
40
|
except ChannelClosed as e:
|
|
35
41
|
logger.error(
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
"Channel closed while getting Dead Letter Exchange '%s'. " "Error: %s",
|
|
43
|
+
cls.DEAD_LETTER_EXCHANGE,
|
|
44
|
+
e,
|
|
38
45
|
)
|
|
39
46
|
raise
|
|
40
47
|
except AMQPError as e:
|
|
41
48
|
logger.error(
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
"AMQP error while getting Dead Letter Exchange '%s'. " "Error: %s",
|
|
50
|
+
cls.DEAD_LETTER_EXCHANGE,
|
|
51
|
+
e,
|
|
44
52
|
)
|
|
45
53
|
raise
|
|
46
54
|
|
|
@@ -71,20 +79,24 @@ class RabbitmqUtils:
|
|
|
71
79
|
)
|
|
72
80
|
except ChannelNotFoundEntity as e:
|
|
73
81
|
logger.error(
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
"Dead Letter Queue '%s' does not exist. "
|
|
83
|
+
"Please use the declare command to create it first. Error: %s",
|
|
84
|
+
cls.DEAD_LETTER_QUEUE,
|
|
85
|
+
e,
|
|
76
86
|
)
|
|
77
87
|
raise
|
|
78
88
|
except ChannelClosed as e:
|
|
79
89
|
logger.error(
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
"Channel closed while getting Dead Letter Queue '%s'. " "Error: %s",
|
|
91
|
+
cls.DEAD_LETTER_QUEUE,
|
|
92
|
+
e,
|
|
82
93
|
)
|
|
83
94
|
raise
|
|
84
95
|
except AMQPError as e:
|
|
85
96
|
logger.error(
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
"AMQP error while getting Dead Letter Queue '%s'. " "Error: %s",
|
|
98
|
+
cls.DEAD_LETTER_QUEUE,
|
|
99
|
+
e,
|
|
88
100
|
)
|
|
89
101
|
raise
|
|
90
102
|
|
|
@@ -120,19 +132,20 @@ class RabbitmqUtils:
|
|
|
120
132
|
return dlx, dlq
|
|
121
133
|
except ChannelNotFoundEntity as e:
|
|
122
134
|
logger.error(
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
"Dead Letter infrastructure does not exist completely. "
|
|
136
|
+
"Please use the declare command to create it first. Error: %s",
|
|
137
|
+
e,
|
|
125
138
|
)
|
|
126
139
|
raise
|
|
127
140
|
except ChannelClosed as e:
|
|
128
141
|
logger.error(
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
"Channel closed while getting Dead Letter infrastructure. " "Error: %s",
|
|
143
|
+
e,
|
|
131
144
|
)
|
|
132
145
|
raise
|
|
133
146
|
except AMQPError as e:
|
|
134
147
|
logger.error(
|
|
135
|
-
|
|
148
|
+
"AMQP error while getting Dead Letter infrastructure. " "Error: %s", e
|
|
136
149
|
)
|
|
137
150
|
raise
|
|
138
151
|
|
|
@@ -161,19 +174,22 @@ class RabbitmqUtils:
|
|
|
161
174
|
return await channel.get_exchange(exchange_name)
|
|
162
175
|
except ChannelNotFoundEntity as e:
|
|
163
176
|
logger.error(
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
"Exchange '%s' does not exist. "
|
|
178
|
+
"Please use the declare command to create it first. Error: %s",
|
|
179
|
+
exchange_name,
|
|
180
|
+
e,
|
|
166
181
|
)
|
|
167
182
|
raise
|
|
168
183
|
except ChannelClosed as e:
|
|
169
184
|
logger.error(
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
"Channel closed while getting exchange '%s'. " "Error: %s",
|
|
186
|
+
exchange_name,
|
|
187
|
+
e,
|
|
172
188
|
)
|
|
173
189
|
raise
|
|
174
190
|
except AMQPError as e:
|
|
175
191
|
logger.error(
|
|
176
|
-
|
|
192
|
+
"AMQP error while getting exchange '%s'. " "Error: %s", exchange_name, e
|
|
177
193
|
)
|
|
178
194
|
raise
|
|
179
195
|
|
|
@@ -206,18 +222,20 @@ class RabbitmqUtils:
|
|
|
206
222
|
return await channel.get_queue(queue_name)
|
|
207
223
|
except ChannelNotFoundEntity as e:
|
|
208
224
|
logger.error(
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
"Queue '%s' does not exist. "
|
|
226
|
+
"Please use the declare command to create it first. Error: %s",
|
|
227
|
+
queue_name,
|
|
228
|
+
e,
|
|
211
229
|
)
|
|
212
230
|
raise
|
|
213
231
|
except ChannelClosed as e:
|
|
214
232
|
logger.error(
|
|
215
|
-
|
|
233
|
+
"Channel closed while getting queue '%s'. " "Error: %s", queue_name, e
|
|
216
234
|
)
|
|
217
235
|
raise
|
|
218
236
|
except AMQPError as e:
|
|
219
237
|
logger.error(
|
|
220
|
-
|
|
238
|
+
"AMQP error while getting queue '%s'. " "Error: %s", queue_name, e
|
|
221
239
|
)
|
|
222
240
|
raise
|
|
223
241
|
|
|
@@ -255,20 +273,24 @@ class RabbitmqUtils:
|
|
|
255
273
|
return await channel.get_queue(queue_name)
|
|
256
274
|
except ChannelNotFoundEntity as e:
|
|
257
275
|
logger.error(
|
|
258
|
-
|
|
259
|
-
|
|
276
|
+
"Scheduler queue '%s' does not exist. "
|
|
277
|
+
"Please use the declare command to create it first. Error: %s",
|
|
278
|
+
queue_name,
|
|
279
|
+
e,
|
|
260
280
|
)
|
|
261
281
|
raise
|
|
262
282
|
except ChannelClosed as e:
|
|
263
283
|
logger.error(
|
|
264
|
-
|
|
265
|
-
|
|
284
|
+
"Channel closed while getting scheduler queue '%s'. " "Error: %s",
|
|
285
|
+
queue_name,
|
|
286
|
+
e,
|
|
266
287
|
)
|
|
267
288
|
raise
|
|
268
289
|
except AMQPError as e:
|
|
269
290
|
logger.error(
|
|
270
|
-
|
|
271
|
-
|
|
291
|
+
"AMQP error while getting scheduler queue '%s'. " "Error: %s",
|
|
292
|
+
queue_name,
|
|
293
|
+
e,
|
|
272
294
|
)
|
|
273
295
|
raise
|
|
274
296
|
|
|
@@ -310,15 +332,17 @@ class RabbitmqUtils:
|
|
|
310
332
|
)
|
|
311
333
|
except ChannelNotFoundEntity:
|
|
312
334
|
# Exchange might not exist, which is fine
|
|
313
|
-
logger.
|
|
314
|
-
|
|
335
|
+
logger.debug(
|
|
336
|
+
"Exchange '%s' does not exist, nothing to delete.", exchange_name
|
|
315
337
|
)
|
|
316
338
|
except ChannelClosed as e:
|
|
317
339
|
logger.warning(
|
|
318
|
-
|
|
340
|
+
"Channel closed while deleting exchange '%s': %s", exchange_name, e
|
|
319
341
|
)
|
|
320
342
|
except AMQPError as e:
|
|
321
|
-
logger.warning(
|
|
343
|
+
logger.warning(
|
|
344
|
+
"AMQP error while deleting exchange '%s': %s", exchange_name, e
|
|
345
|
+
)
|
|
322
346
|
|
|
323
347
|
@classmethod
|
|
324
348
|
async def delete_queue(
|
|
@@ -339,8 +363,57 @@ class RabbitmqUtils:
|
|
|
339
363
|
)
|
|
340
364
|
except ChannelNotFoundEntity:
|
|
341
365
|
# Queue might not exist, which is fine
|
|
342
|
-
logger.
|
|
366
|
+
logger.debug("Queue '%s' does not exist, nothing to delete.", queue_name)
|
|
367
|
+
except ChannelClosed as e:
|
|
368
|
+
logger.warning(
|
|
369
|
+
"Channel closed while deleting queue '%s': %s", queue_name, e
|
|
370
|
+
)
|
|
371
|
+
except AMQPError as e:
|
|
372
|
+
logger.warning("AMQP error while deleting queue '%s': %s", queue_name, e)
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
async def get_dl_queue_message_count(cls, channel: AbstractChannel) -> int:
|
|
376
|
+
"""
|
|
377
|
+
Get the message count in the Dead Letter Queue.
|
|
378
|
+
Returns 0 if the queue doesn't exist.
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
await channel.get_queue(cls.DEAD_LETTER_QUEUE)
|
|
382
|
+
# The declaration property contains the message count
|
|
383
|
+
queue_info = await channel.declare_queue(
|
|
384
|
+
cls.DEAD_LETTER_QUEUE, passive=True
|
|
385
|
+
)
|
|
386
|
+
return queue_info.declaration_result.message_count or 0
|
|
387
|
+
except ChannelNotFoundEntity:
|
|
388
|
+
logger.debug("Dead Letter Queue does not exist.")
|
|
389
|
+
return 0
|
|
390
|
+
except ChannelClosed as e:
|
|
391
|
+
logger.error("Channel closed while getting DLQ message count: %s", e)
|
|
392
|
+
raise
|
|
393
|
+
except AMQPError as e:
|
|
394
|
+
logger.error("AMQP error while getting DLQ message count: %s", e)
|
|
395
|
+
raise
|
|
396
|
+
|
|
397
|
+
@classmethod
|
|
398
|
+
async def purge_dl_queue(cls, channel: AbstractChannel) -> int:
|
|
399
|
+
"""
|
|
400
|
+
Purge all messages from the Dead Letter Queue.
|
|
401
|
+
Returns the number of messages purged.
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
queue = await channel.get_queue(cls.DEAD_LETTER_QUEUE)
|
|
405
|
+
result = await queue.purge()
|
|
406
|
+
return result.message_count or 0
|
|
407
|
+
except ChannelNotFoundEntity as e:
|
|
408
|
+
logger.error(
|
|
409
|
+
"Dead Letter Queue '%s' does not exist. Error: %s",
|
|
410
|
+
cls.DEAD_LETTER_QUEUE,
|
|
411
|
+
e,
|
|
412
|
+
)
|
|
413
|
+
raise
|
|
343
414
|
except ChannelClosed as e:
|
|
344
|
-
logger.
|
|
415
|
+
logger.error("Channel closed while purging Dead Letter Queue: %s", e)
|
|
416
|
+
raise
|
|
345
417
|
except AMQPError as e:
|
|
346
|
-
logger.
|
|
418
|
+
logger.error("AMQP error while purging Dead Letter Queue: %s", e)
|
|
419
|
+
raise
|
jararaca/utils/retry.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import asyncio
|
|
2
6
|
import logging
|
|
3
7
|
import random
|
|
@@ -10,7 +14,7 @@ P = ParamSpec("P")
|
|
|
10
14
|
T = TypeVar("T")
|
|
11
15
|
|
|
12
16
|
|
|
13
|
-
class
|
|
17
|
+
class RetryPolicy:
|
|
14
18
|
"""Configuration for the retry mechanism."""
|
|
15
19
|
|
|
16
20
|
def __init__(
|
|
@@ -45,7 +49,7 @@ async def retry_with_backoff(
|
|
|
45
49
|
fn: Callable[[], Awaitable[T]],
|
|
46
50
|
# args: P.args,
|
|
47
51
|
# kwargs: P.kwargs,
|
|
48
|
-
|
|
52
|
+
retry_policy: RetryPolicy,
|
|
49
53
|
on_retry_callback: Optional[Callable[[int, E, float], None]] = None,
|
|
50
54
|
retry_exceptions: tuple[type[E], ...] = (),
|
|
51
55
|
) -> T:
|
|
@@ -66,38 +70,39 @@ async def retry_with_backoff(
|
|
|
66
70
|
Raises:
|
|
67
71
|
The last exception encountered if all retries fail
|
|
68
72
|
"""
|
|
69
|
-
if retry_config is None:
|
|
70
|
-
retry_config = RetryConfig()
|
|
71
73
|
|
|
72
74
|
last_exception = None
|
|
73
|
-
delay =
|
|
75
|
+
delay = retry_policy.initial_delay
|
|
74
76
|
|
|
75
|
-
for retry_count in range(
|
|
77
|
+
for retry_count in range(retry_policy.max_retries + 1):
|
|
76
78
|
try:
|
|
77
79
|
return await fn()
|
|
78
80
|
except retry_exceptions as e:
|
|
79
81
|
last_exception = e
|
|
80
82
|
|
|
81
|
-
if retry_count >=
|
|
83
|
+
if retry_count >= retry_policy.max_retries:
|
|
82
84
|
logger.error(
|
|
83
|
-
|
|
85
|
+
"Max retries (%s) exceeded: %s", retry_policy.max_retries, e
|
|
84
86
|
)
|
|
85
87
|
raise
|
|
86
88
|
|
|
87
89
|
# Calculate next delay with exponential backoff
|
|
88
90
|
if retry_count > 0: # Don't increase delay on the first failure
|
|
89
|
-
delay = min(delay *
|
|
91
|
+
delay = min(delay * retry_policy.backoff_factor, retry_policy.max_delay)
|
|
90
92
|
|
|
91
93
|
# Apply jitter if configured (±25% randomness)
|
|
92
|
-
if
|
|
94
|
+
if retry_policy.jitter:
|
|
93
95
|
jitter_amount = delay * 0.25
|
|
94
96
|
delay = delay + random.uniform(-jitter_amount, jitter_amount)
|
|
95
97
|
# Ensure delay doesn't go negative due to jitter
|
|
96
98
|
delay = max(delay, 0.1)
|
|
97
99
|
|
|
98
100
|
logger.warning(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
"Retry %s/%s after error: %s. Retrying in %.2fs",
|
|
102
|
+
retry_count + 1,
|
|
103
|
+
retry_policy.max_retries,
|
|
104
|
+
e,
|
|
105
|
+
delay,
|
|
101
106
|
)
|
|
102
107
|
|
|
103
108
|
# Call the optional retry callback if provided
|
|
@@ -113,7 +118,7 @@ async def retry_with_backoff(
|
|
|
113
118
|
|
|
114
119
|
|
|
115
120
|
def with_retry(
|
|
116
|
-
|
|
121
|
+
retry_policy: RetryPolicy,
|
|
117
122
|
retry_exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
118
123
|
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
|
119
124
|
"""
|
|
@@ -132,7 +137,7 @@ def with_retry(
|
|
|
132
137
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
133
138
|
return await retry_with_backoff(
|
|
134
139
|
lambda: fn(*args, **kwargs),
|
|
135
|
-
|
|
140
|
+
retry_policy=retry_policy,
|
|
136
141
|
retry_exceptions=retry_exceptions,
|
|
137
142
|
)
|
|
138
143
|
|