ergon-framework-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ergon/__init__.py +13 -0
- ergon/bootstrap/src/__project__/__init__.py +0 -0
- ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +124 -0
- ergon/bootstrap/src/__project__/_observability/grafana.yaml +17 -0
- ergon/bootstrap/src/__project__/_observability/loki.yaml +48 -0
- ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +53 -0
- ergon/bootstrap/src/__project__/_observability/prometheus.yaml +11 -0
- ergon/bootstrap/src/__project__/_observability/tempo.yaml +24 -0
- ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
- ergon/bootstrap/src/__project__/main.py +9 -0
- ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
- ergon/bootstrap/src/__project__/tasks/constants.py +13 -0
- ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
- ergon/bootstrap/src/__project__/tasks/example_task/config.py +4 -0
- ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +4 -0
- ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +4 -0
- ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +5 -0
- ergon/bootstrap/src/__project__/tasks/example_task/task.py +1 -0
- ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
- ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
- ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
- ergon/bootstrap/src/__project__/tasks/settings.py +5 -0
- ergon/cli.py +174 -0
- ergon/connector/__init__.py +64 -0
- ergon/connector/connector.py +97 -0
- ergon/connector/excel/__init__.py +18 -0
- ergon/connector/excel/connector.py +175 -0
- ergon/connector/excel/models.py +24 -0
- ergon/connector/excel/service.py +98 -0
- ergon/connector/pipefy/__init__.py +21 -0
- ergon/connector/pipefy/async_connector.py +48 -0
- ergon/connector/pipefy/async_service.py +907 -0
- ergon/connector/pipefy/connector.py +36 -0
- ergon/connector/pipefy/models.py +48 -0
- ergon/connector/pipefy/service.py +1016 -0
- ergon/connector/pipefy/version.py +1 -0
- ergon/connector/postgres/__init__.py +11 -0
- ergon/connector/postgres/async_connector.py +119 -0
- ergon/connector/postgres/async_service.py +116 -0
- ergon/connector/postgres/models.py +34 -0
- ergon/connector/rabbitmq/__init__.py +25 -0
- ergon/connector/rabbitmq/async_connector.py +120 -0
- ergon/connector/rabbitmq/async_service.py +417 -0
- ergon/connector/rabbitmq/connector.py +54 -0
- ergon/connector/rabbitmq/helper.py +14 -0
- ergon/connector/rabbitmq/models.py +92 -0
- ergon/connector/rabbitmq/service.py +199 -0
- ergon/connector/sqs/__init__.py +15 -0
- ergon/connector/sqs/async_connector.py +120 -0
- ergon/connector/sqs/async_service.py +246 -0
- ergon/connector/sqs/connector.py +120 -0
- ergon/connector/sqs/models.py +36 -0
- ergon/connector/sqs/service.py +219 -0
- ergon/connector/transaction.py +14 -0
- ergon/py.typed +0 -0
- ergon/service/__init__.py +5 -0
- ergon/service/service.py +17 -0
- ergon/task/__init__.py +13 -0
- ergon/task/base.py +222 -0
- ergon/task/exceptions.py +217 -0
- ergon/task/helpers.py +691 -0
- ergon/task/manager.py +85 -0
- ergon/task/mixins/__init__.py +13 -0
- ergon/task/mixins/consumer.py +858 -0
- ergon/task/mixins/metrics.py +457 -0
- ergon/task/mixins/producer.py +486 -0
- ergon/task/policies.py +229 -0
- ergon/task/runner.py +386 -0
- ergon/task/utils.py +64 -0
- ergon/telemetry/__init__.py +7 -0
- ergon/telemetry/_resource.py +13 -0
- ergon/telemetry/logging.py +370 -0
- ergon/telemetry/metrics.py +101 -0
- ergon/telemetry/tracing.py +152 -0
- ergon/utils/__init__.py +5 -0
- ergon/utils/env.py +26 -0
- ergon_framework_python-0.1.0.dist-info/METADATA +449 -0
- ergon_framework_python-0.1.0.dist-info/RECORD +82 -0
- ergon_framework_python-0.1.0.dist-info/WHEEL +5 -0
- ergon_framework_python-0.1.0.dist-info/entry_points.txt +2 -0
- ergon_framework_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- ergon_framework_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from concurrent import futures
|
|
6
|
+
from typing import Any, List
|
|
7
|
+
|
|
8
|
+
from more_itertools import chunked
|
|
9
|
+
|
|
10
|
+
from ... import connector, telemetry
|
|
11
|
+
from .. import base, exceptions, helpers, policies
|
|
12
|
+
from . import metrics as mixin_metrics
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
tracer = telemetry.tracing.get_tracer(__name__)
|
|
16
|
+
|
|
17
|
+
# -------------------------------------------------------------------
|
|
18
|
+
# PRODUCER MIXIN (SYNC)
|
|
19
|
+
# -------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProducerMixin(ABC):
|
|
23
|
+
name: str
|
|
24
|
+
|
|
25
|
+
# -------------------------------------------------------------------
|
|
26
|
+
# HOOKS
|
|
27
|
+
# -------------------------------------------------------------------
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def prepare_transaction(self, transaction: connector.Transaction) -> Any:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def handle_prepare_success(self, transaction: connector.Transaction, result: Any):
|
|
33
|
+
logger.debug(f"[{self.name}] SUCCESS → {transaction.id}")
|
|
34
|
+
|
|
35
|
+
def handle_prepare_exception(self, transaction: connector.Transaction, exc: exceptions.TransactionException):
|
|
36
|
+
logger.error(f"[{self.name}] EXCEPTION → {transaction.id}: {exc}")
|
|
37
|
+
|
|
38
|
+
# -------------------------------------------------------------------
|
|
39
|
+
# PRODUCE 1 ITEM (FULL RETRY LIFECYCLE)
|
|
40
|
+
# -------------------------------------------------------------------
|
|
41
|
+
def _start_producing(self, transaction: connector.Transaction, policy: policies.ProducerPolicy):
|
|
42
|
+
"""
|
|
43
|
+
PRODUCE → SUCCESS | EXCEPTION
|
|
44
|
+
"""
|
|
45
|
+
tx_start = time.perf_counter()
|
|
46
|
+
final_status = "success"
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# -----------------------
|
|
50
|
+
# 1) PREPARE
|
|
51
|
+
# -----------------------
|
|
52
|
+
prepare_success, prepare_result = self._handle_prepare(transaction, policy.prepare)
|
|
53
|
+
|
|
54
|
+
# -----------------------
|
|
55
|
+
# 2) EXCEPTION HANDLER
|
|
56
|
+
# -----------------------
|
|
57
|
+
if not prepare_success:
|
|
58
|
+
final_status = "exception"
|
|
59
|
+
if isinstance(prepare_result, exceptions.TransactionException):
|
|
60
|
+
prepare_result = prepare_result
|
|
61
|
+
elif isinstance(prepare_result, futures.TimeoutError):
|
|
62
|
+
prepare_result = exceptions.TransactionException(
|
|
63
|
+
str(prepare_result), exceptions.ExceptionType.TIMEOUT
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
prepare_result = exceptions.TransactionException(
|
|
67
|
+
str(prepare_result), exceptions.ExceptionType.SYSTEM
|
|
68
|
+
)
|
|
69
|
+
return self._handle_prepare_exception(transaction, prepare_result, policy.exception)
|
|
70
|
+
|
|
71
|
+
# -----------------------
|
|
72
|
+
# 3) SUCCESS HANDLER
|
|
73
|
+
# -----------------------
|
|
74
|
+
success_success, success_result = self._handle_prepare_success(
|
|
75
|
+
transaction, prepare_result, policy.success, policy.exception
|
|
76
|
+
)
|
|
77
|
+
if not success_success:
|
|
78
|
+
final_status = "exception"
|
|
79
|
+
if isinstance(success_result, exceptions.TransactionException):
|
|
80
|
+
success_result = success_result
|
|
81
|
+
elif isinstance(success_result, futures.TimeoutError):
|
|
82
|
+
success_result = exceptions.TransactionException(
|
|
83
|
+
str(success_result), exceptions.ExceptionType.TIMEOUT
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
success_result = exceptions.TransactionException(
|
|
87
|
+
str(success_result), exceptions.ExceptionType.SYSTEM
|
|
88
|
+
)
|
|
89
|
+
return self._handle_prepare_exception(transaction, success_result, policy.exception)
|
|
90
|
+
|
|
91
|
+
return True, success_result
|
|
92
|
+
finally:
|
|
93
|
+
# Record transaction-level metrics
|
|
94
|
+
tx_duration = time.perf_counter() - tx_start
|
|
95
|
+
mixin_metrics.record_producer_transaction(
|
|
96
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
97
|
+
transaction_id=transaction.id,
|
|
98
|
+
duration=tx_duration,
|
|
99
|
+
status=final_status,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# -------------------------------------------------------------------
|
|
103
|
+
# PREPARE HANDLER
|
|
104
|
+
# -------------------------------------------------------------------
|
|
105
|
+
def _handle_prepare(self, transaction: connector.Transaction, policy: policies.PreparePolicy):
|
|
106
|
+
logger.debug(f"[Producer] _handle_prepare called for transaction {transaction.id}")
|
|
107
|
+
stage_start = time.perf_counter()
|
|
108
|
+
success, result = helpers.run_fn(
|
|
109
|
+
fn=lambda: self.prepare_transaction(transaction),
|
|
110
|
+
retry=policy.retry,
|
|
111
|
+
trace_name=f"{self.__class__.__name__}.prepare",
|
|
112
|
+
trace_attrs={"transaction": transaction.id},
|
|
113
|
+
)
|
|
114
|
+
# Record lifecycle metrics
|
|
115
|
+
mixin_metrics.record_producer_lifecycle(
|
|
116
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
117
|
+
stage="prepare",
|
|
118
|
+
duration=time.perf_counter() - stage_start,
|
|
119
|
+
outcome="ok" if success else "error",
|
|
120
|
+
)
|
|
121
|
+
return success, result
|
|
122
|
+
|
|
123
|
+
# -------------------------------------------------------------------
|
|
124
|
+
# SUCCESS HANDLER
|
|
125
|
+
# -------------------------------------------------------------------
|
|
126
|
+
def _handle_prepare_success(
|
|
127
|
+
self,
|
|
128
|
+
transaction: connector.Transaction,
|
|
129
|
+
result: Any,
|
|
130
|
+
policy: policies.SuccessPolicy,
|
|
131
|
+
exception_policy: policies.ExceptionPolicy,
|
|
132
|
+
):
|
|
133
|
+
stage_start = time.perf_counter()
|
|
134
|
+
success, handler_result = helpers.run_fn(
|
|
135
|
+
fn=lambda: self.handle_prepare_success(transaction, result),
|
|
136
|
+
retry=policy.retry,
|
|
137
|
+
trace_name=f"{self.__class__.__name__}.handle_prepare_success",
|
|
138
|
+
trace_attrs={"transaction": transaction.id},
|
|
139
|
+
)
|
|
140
|
+
# Record lifecycle metrics
|
|
141
|
+
mixin_metrics.record_producer_lifecycle(
|
|
142
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
143
|
+
stage="success",
|
|
144
|
+
duration=time.perf_counter() - stage_start,
|
|
145
|
+
outcome="ok" if success else "error",
|
|
146
|
+
)
|
|
147
|
+
return success, handler_result
|
|
148
|
+
|
|
149
|
+
# -------------------------------------------------------------------
|
|
150
|
+
# EXCEPTION HANDLER
|
|
151
|
+
# -------------------------------------------------------------------
|
|
152
|
+
def _handle_prepare_exception(
|
|
153
|
+
self,
|
|
154
|
+
transaction: connector.Transaction,
|
|
155
|
+
exc: exceptions.TransactionException,
|
|
156
|
+
policy: policies.ExceptionPolicy,
|
|
157
|
+
):
|
|
158
|
+
stage_start = time.perf_counter()
|
|
159
|
+
success, result = helpers.run_fn(
|
|
160
|
+
fn=lambda: self.handle_prepare_exception(transaction, exc),
|
|
161
|
+
retry=policy.retry,
|
|
162
|
+
trace_name=f"{self.__class__.__name__}.handle_prepare_exception",
|
|
163
|
+
trace_attrs={"transaction": transaction.id},
|
|
164
|
+
)
|
|
165
|
+
# Record lifecycle metrics
|
|
166
|
+
mixin_metrics.record_producer_lifecycle(
|
|
167
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
168
|
+
stage="exception",
|
|
169
|
+
duration=time.perf_counter() - stage_start,
|
|
170
|
+
outcome="ok" if success else "error",
|
|
171
|
+
)
|
|
172
|
+
return success, result
|
|
173
|
+
|
|
174
|
+
# -------------------------------------------------------------------
|
|
175
|
+
# PUBLIC API — PRODUCE MANY
|
|
176
|
+
# -------------------------------------------------------------------
|
|
177
|
+
def produce_transactions(
|
|
178
|
+
self,
|
|
179
|
+
transactions: List[connector.Transaction],
|
|
180
|
+
policy: policies.ProducerPolicy | None = None,
|
|
181
|
+
):
|
|
182
|
+
if policy is None:
|
|
183
|
+
policy = policies.ProducerPolicy()
|
|
184
|
+
|
|
185
|
+
def _produce():
|
|
186
|
+
start_time = time.perf_counter()
|
|
187
|
+
processed = 0
|
|
188
|
+
executor = futures.ThreadPoolExecutor(max_workers=policy.loop.concurrency.value)
|
|
189
|
+
|
|
190
|
+
# ============================================================
|
|
191
|
+
# SUBMIT FUNCTION
|
|
192
|
+
# ============================================================
|
|
193
|
+
def submit_start_producing(tr, pol):
|
|
194
|
+
return helpers.run_fn(
|
|
195
|
+
fn=lambda: self._start_producing(transaction=tr, policy=pol),
|
|
196
|
+
executor=executor,
|
|
197
|
+
trace_name=f"{self.__class__.__name__}.start_producing",
|
|
198
|
+
trace_attrs={"transaction_id": tr.id},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# ============================================================
|
|
202
|
+
# PRODUCE LOOP
|
|
203
|
+
# ============================================================
|
|
204
|
+
batches = list(chunked(transactions, policy.loop.batch.size))
|
|
205
|
+
for batch_number, batch in enumerate(batches, 1):
|
|
206
|
+
# Record batch metric
|
|
207
|
+
mixin_metrics.record_producer_batch(
|
|
208
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
209
|
+
batch_number=batch_number,
|
|
210
|
+
batch_size=len(batch),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def submissions():
|
|
214
|
+
for tr in batch:
|
|
215
|
+
yield lambda tr=tr: submit_start_producing(tr, policy)
|
|
216
|
+
|
|
217
|
+
count = helpers.multithread_execute(
|
|
218
|
+
submissions=submissions(),
|
|
219
|
+
concurrency=policy.loop.concurrency.value,
|
|
220
|
+
limit=policy.loop.limit,
|
|
221
|
+
timeout=policy.transaction_runtime.timeout,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
processed += count
|
|
225
|
+
|
|
226
|
+
if policy.loop.limit and processed >= policy.loop.limit:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
executor.shutdown()
|
|
230
|
+
elapsed_time = time.perf_counter() - start_time
|
|
231
|
+
logger.info(f"[Produce] Finished. Processed={processed} in {elapsed_time:.2f} seconds")
|
|
232
|
+
return processed
|
|
233
|
+
|
|
234
|
+
success, result = helpers.run_fn(
|
|
235
|
+
fn=lambda: _produce(),
|
|
236
|
+
retry=policies.RetryPolicy(timeout=policy.loop.timeout),
|
|
237
|
+
trace_name=f"{self.__class__.__name__}.produce_transactions",
|
|
238
|
+
trace_attrs={"count": len(transactions)},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not success:
|
|
242
|
+
if isinstance(result, futures.TimeoutError):
|
|
243
|
+
raise exceptions.ProducerLoopTimeoutException(str(result))
|
|
244
|
+
raise result
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class ProducerTask(ProducerMixin, base.BaseTask):
|
|
249
|
+
"""
|
|
250
|
+
Backwards-compatible producer task.
|
|
251
|
+
You can still inherit from this if you're only a producer.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# -------------------------------------------------------------------
|
|
258
|
+
# ASYNC PRODUCER MIXIN
|
|
259
|
+
# -------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class AsyncProducerMixin(ABC):
|
|
263
|
+
name: str
|
|
264
|
+
|
|
265
|
+
# -------------------------------------------------------------------
|
|
266
|
+
# HOOKS
|
|
267
|
+
# -------------------------------------------------------------------
|
|
268
|
+
@abstractmethod
|
|
269
|
+
async def prepare_transaction(self, transaction: connector.Transaction) -> Any:
|
|
270
|
+
raise NotImplementedError
|
|
271
|
+
|
|
272
|
+
async def handle_prepare_success(self, transaction: connector.Transaction, result: Any):
|
|
273
|
+
logger.debug(f"[{self.name}] SUCCESS → {transaction.id}")
|
|
274
|
+
|
|
275
|
+
async def handle_prepare_exception(self, transaction: connector.Transaction, exc: exceptions.TransactionException):
|
|
276
|
+
logger.error(f"[{self.name}] EXCEPTION → {transaction.id}: {exc}")
|
|
277
|
+
|
|
278
|
+
# -------------------------------------------------------------------
|
|
279
|
+
# PRODUCE 1 ITEM (FULL RETRY LIFECYCLE)
|
|
280
|
+
# -------------------------------------------------------------------
|
|
281
|
+
async def _start_producing(self, transaction: connector.Transaction, policy: policies.ProducerPolicy):
|
|
282
|
+
"""
|
|
283
|
+
PRODUCE → SUCCESS | EXCEPTION
|
|
284
|
+
"""
|
|
285
|
+
tx_start = time.perf_counter()
|
|
286
|
+
final_status = "success"
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
# -----------------------
|
|
290
|
+
# 1) PREPARE
|
|
291
|
+
# -----------------------
|
|
292
|
+
prepare_success, prepare_result = await self._handle_prepare(transaction, policy.prepare)
|
|
293
|
+
|
|
294
|
+
# -----------------------
|
|
295
|
+
# 2) EXCEPTION HANDLER
|
|
296
|
+
# -----------------------
|
|
297
|
+
if not prepare_success:
|
|
298
|
+
final_status = "exception"
|
|
299
|
+
if isinstance(prepare_result, exceptions.TransactionException):
|
|
300
|
+
prepare_result = prepare_result
|
|
301
|
+
elif isinstance(prepare_result, (asyncio.TimeoutError, futures.TimeoutError)):
|
|
302
|
+
prepare_result = exceptions.TransactionException(
|
|
303
|
+
str(prepare_result), exceptions.ExceptionType.TIMEOUT
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
prepare_result = exceptions.TransactionException(
|
|
307
|
+
str(prepare_result), exceptions.ExceptionType.SYSTEM
|
|
308
|
+
)
|
|
309
|
+
return await self._handle_prepare_exception(transaction, prepare_result, policy.exception)
|
|
310
|
+
|
|
311
|
+
# -----------------------
|
|
312
|
+
# 3) SUCCESS HANDLER
|
|
313
|
+
# -----------------------
|
|
314
|
+
success_success, success_result = await self._handle_prepare_success(
|
|
315
|
+
transaction, prepare_result, policy.success, policy.exception
|
|
316
|
+
)
|
|
317
|
+
if not success_success:
|
|
318
|
+
final_status = "exception"
|
|
319
|
+
if isinstance(success_result, exceptions.TransactionException):
|
|
320
|
+
success_result = success_result
|
|
321
|
+
elif isinstance(success_result, (asyncio.TimeoutError, futures.TimeoutError)):
|
|
322
|
+
success_result = exceptions.TransactionException(
|
|
323
|
+
str(success_result), exceptions.ExceptionType.TIMEOUT
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
success_result = exceptions.TransactionException(
|
|
327
|
+
str(success_result), exceptions.ExceptionType.SYSTEM
|
|
328
|
+
)
|
|
329
|
+
return await self._handle_prepare_exception(transaction, success_result, policy.exception)
|
|
330
|
+
|
|
331
|
+
return True, success_result
|
|
332
|
+
finally:
|
|
333
|
+
tx_duration = time.perf_counter() - tx_start
|
|
334
|
+
mixin_metrics.record_producer_transaction(
|
|
335
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
336
|
+
transaction_id=transaction.id,
|
|
337
|
+
duration=tx_duration,
|
|
338
|
+
status=final_status,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# -------------------------------------------------------------------
|
|
342
|
+
# PREPARE HANDLER
|
|
343
|
+
# -------------------------------------------------------------------
|
|
344
|
+
async def _handle_prepare(self, transaction: connector.Transaction, policy: policies.PreparePolicy):
|
|
345
|
+
logger.debug(f"[Producer] _handle_prepare called for transaction {transaction.id}")
|
|
346
|
+
stage_start = time.perf_counter()
|
|
347
|
+
success, result = await helpers.run_fn_async(
|
|
348
|
+
fn=lambda: self.prepare_transaction(transaction),
|
|
349
|
+
retry=policy.retry,
|
|
350
|
+
trace_name=f"{self.__class__.__name__}.prepare",
|
|
351
|
+
trace_attrs={"transaction": transaction.id},
|
|
352
|
+
)
|
|
353
|
+
mixin_metrics.record_producer_lifecycle(
|
|
354
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
355
|
+
stage="prepare",
|
|
356
|
+
duration=time.perf_counter() - stage_start,
|
|
357
|
+
outcome="ok" if success else "error",
|
|
358
|
+
)
|
|
359
|
+
return success, result
|
|
360
|
+
|
|
361
|
+
# -------------------------------------------------------------------
|
|
362
|
+
# SUCCESS HANDLER
|
|
363
|
+
# -------------------------------------------------------------------
|
|
364
|
+
async def _handle_prepare_success(
|
|
365
|
+
self,
|
|
366
|
+
transaction: connector.Transaction,
|
|
367
|
+
result: Any,
|
|
368
|
+
policy: policies.SuccessPolicy,
|
|
369
|
+
exception_policy: policies.ExceptionPolicy,
|
|
370
|
+
):
|
|
371
|
+
stage_start = time.perf_counter()
|
|
372
|
+
success, handler_result = await helpers.run_fn_async(
|
|
373
|
+
fn=lambda: self.handle_prepare_success(transaction, result),
|
|
374
|
+
retry=policy.retry,
|
|
375
|
+
trace_name=f"{self.__class__.__name__}.handle_prepare_success",
|
|
376
|
+
trace_attrs={"transaction": transaction.id},
|
|
377
|
+
)
|
|
378
|
+
mixin_metrics.record_producer_lifecycle(
|
|
379
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
380
|
+
stage="success",
|
|
381
|
+
duration=time.perf_counter() - stage_start,
|
|
382
|
+
outcome="ok" if success else "error",
|
|
383
|
+
)
|
|
384
|
+
return success, handler_result
|
|
385
|
+
|
|
386
|
+
# -------------------------------------------------------------------
|
|
387
|
+
# EXCEPTION HANDLER
|
|
388
|
+
# -------------------------------------------------------------------
|
|
389
|
+
async def _handle_prepare_exception(
|
|
390
|
+
self,
|
|
391
|
+
transaction: connector.Transaction,
|
|
392
|
+
exc: exceptions.TransactionException,
|
|
393
|
+
policy: policies.ExceptionPolicy,
|
|
394
|
+
):
|
|
395
|
+
stage_start = time.perf_counter()
|
|
396
|
+
success, result = await helpers.run_fn_async(
|
|
397
|
+
fn=lambda: self.handle_prepare_exception(transaction, exc),
|
|
398
|
+
retry=policy.retry,
|
|
399
|
+
trace_name=f"{self.__class__.__name__}.handle_prepare_exception",
|
|
400
|
+
trace_attrs={"transaction": transaction.id},
|
|
401
|
+
)
|
|
402
|
+
mixin_metrics.record_producer_lifecycle(
|
|
403
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
404
|
+
stage="exception",
|
|
405
|
+
duration=time.perf_counter() - stage_start,
|
|
406
|
+
outcome="ok" if success else "error",
|
|
407
|
+
)
|
|
408
|
+
return success, result
|
|
409
|
+
|
|
410
|
+
# -------------------------------------------------------------------
|
|
411
|
+
# PUBLIC API — PRODUCE MANY
|
|
412
|
+
# -------------------------------------------------------------------
|
|
413
|
+
async def produce_transactions(
|
|
414
|
+
self,
|
|
415
|
+
transactions: List[connector.Transaction],
|
|
416
|
+
policy: policies.ProducerPolicy | None = None,
|
|
417
|
+
):
|
|
418
|
+
if policy is None:
|
|
419
|
+
policy = policies.ProducerPolicy()
|
|
420
|
+
|
|
421
|
+
async def _produce():
|
|
422
|
+
start_time = time.perf_counter()
|
|
423
|
+
processed = 0
|
|
424
|
+
|
|
425
|
+
# ============================================================
|
|
426
|
+
# SUBMIT FUNCTION
|
|
427
|
+
# ============================================================
|
|
428
|
+
async def submit_start_producing(tr, pol):
|
|
429
|
+
return await helpers.run_fn_async(
|
|
430
|
+
fn=lambda: self._start_producing(transaction=tr, policy=pol),
|
|
431
|
+
trace_name=f"{self.__class__.__name__}.start_producing",
|
|
432
|
+
trace_attrs={"transaction_id": tr.id},
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# ============================================================
|
|
436
|
+
# PRODUCE LOOP
|
|
437
|
+
# ============================================================
|
|
438
|
+
batches = list(chunked(transactions, policy.loop.batch.size))
|
|
439
|
+
for batch_number, batch in enumerate(batches, 1):
|
|
440
|
+
mixin_metrics.record_producer_batch(
|
|
441
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
442
|
+
batch_number=batch_number,
|
|
443
|
+
batch_size=len(batch),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def submissions():
|
|
447
|
+
for tr in batch:
|
|
448
|
+
yield lambda tr=tr: asyncio.create_task(submit_start_producing(tr, policy))
|
|
449
|
+
|
|
450
|
+
count = await helpers.async_execute(
|
|
451
|
+
submissions=submissions(),
|
|
452
|
+
concurrency=policy.loop.concurrency.value,
|
|
453
|
+
limit=policy.loop.limit,
|
|
454
|
+
timeout=policy.transaction_runtime.timeout,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
processed += count
|
|
458
|
+
|
|
459
|
+
if policy.loop.limit and processed >= policy.loop.limit:
|
|
460
|
+
break
|
|
461
|
+
|
|
462
|
+
elapsed_time = time.perf_counter() - start_time
|
|
463
|
+
logger.info(f"[Produce] Finished. Processed={processed} in {elapsed_time:.2f} seconds")
|
|
464
|
+
return processed
|
|
465
|
+
|
|
466
|
+
success, result = await helpers.run_fn_async(
|
|
467
|
+
fn=_produce,
|
|
468
|
+
retry=policies.RetryPolicy(timeout=policy.loop.timeout),
|
|
469
|
+
trace_name=f"{self.__class__.__name__}.produce_transactions",
|
|
470
|
+
trace_attrs={"count": len(transactions)},
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if not success:
|
|
474
|
+
if isinstance(result, asyncio.TimeoutError):
|
|
475
|
+
raise exceptions.ProducerLoopTimeoutException(str(result))
|
|
476
|
+
raise result
|
|
477
|
+
return result
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class AsyncProducerTask(AsyncProducerMixin, base.BaseAsyncTask):
|
|
481
|
+
"""
|
|
482
|
+
Async producer task.
|
|
483
|
+
You can still inherit from this if you're only an async producer.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
pass
|