ergon-framework-python 0.1.7__tar.gz → 0.1.9__tar.gz
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_framework_python-0.1.7 → ergon_framework_python-0.1.9}/PKG-INFO +1 -1
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/pyproject.toml +2 -2
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/__init__.py +1 -1
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/async_service.py +29 -7
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/consumer.py +146 -7
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/policies.py +10 -1
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/runner.py +39 -16
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/PKG-INFO +1 -1
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/tests/test_smoke.py +1 -1
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/LICENSE +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/README.md +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/setup.cfg +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/main.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/cli.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/_client.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/_operations.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/utils.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/async_auth_service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/async_connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/async_service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/auth_service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/utils.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/async_connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/async_service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/version.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/async_connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/async_service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/helper.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/async_connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/async_service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/connector.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/models.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/transaction.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/py.typed +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/service/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/service/service.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/base.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/exceptions.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/helpers.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/liveness.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/manager.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/metrics.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/producer.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/utils.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/_resource.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/logging.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/metrics.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/tracing.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/utils/__init__.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/utils/env.py +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/SOURCES.txt +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/requires.txt +0 -0
- {ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ergon-framework-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.9"
|
|
4
4
|
description = "Ergon internal task-oriented project bootstrapper"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -47,7 +47,7 @@ dev = [
|
|
|
47
47
|
"pyright>=1.1.390",
|
|
48
48
|
"pytest>=8.0.0",
|
|
49
49
|
"pytest-asyncio>=0.25.0",
|
|
50
|
-
"ruff>=0.6.0",
|
|
50
|
+
"ruff>=0.6.0,<0.16",
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
[build-system]
|
|
@@ -155,11 +155,33 @@ class AsyncRabbitMQService:
|
|
|
155
155
|
|
|
156
156
|
connection = self._connection
|
|
157
157
|
self._connection = None
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
await self._close_connection_safely(connection, reason)
|
|
159
|
+
|
|
160
|
+
async def _close_connection_safely(
|
|
161
|
+
self,
|
|
162
|
+
connection: Optional[AbstractRobustConnection],
|
|
163
|
+
reason: str,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Stop transport and robust-reconnect ownership within a fixed deadline.
|
|
166
|
+
|
|
167
|
+
``RobustConnection.is_closed`` only describes its current transport. Its
|
|
168
|
+
background reconnection task can still be alive after the broker closes
|
|
169
|
+
that transport, so ``close()`` must always run for an owned connection.
|
|
170
|
+
"""
|
|
171
|
+
if connection is None:
|
|
172
|
+
return
|
|
173
|
+
close_timeout = min(5.0, self.client.connect_timeout)
|
|
174
|
+
try:
|
|
175
|
+
async with timeout_after(close_timeout):
|
|
160
176
|
await connection.close()
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
except TimeoutError:
|
|
178
|
+
logger.error(
|
|
179
|
+
"Timed out closing RabbitMQ connection after %.1fs (%s)",
|
|
180
|
+
close_timeout,
|
|
181
|
+
reason,
|
|
182
|
+
)
|
|
183
|
+
except Exception as exc: # noqa: BLE001 - best-effort reset
|
|
184
|
+
logger.warning("Error closing RabbitMQ connection (%s): %r", reason, exc)
|
|
163
185
|
|
|
164
186
|
def _invalidate_consume_channel(self, reason: str = "explicit invalidation") -> None:
|
|
165
187
|
"""Drop cached consume channel + queue/exchange handles.
|
|
@@ -752,9 +774,9 @@ class AsyncRabbitMQService:
|
|
|
752
774
|
logger.warning("Error closing %s: %r", attr_name, exc)
|
|
753
775
|
setattr(self, attr_name, None)
|
|
754
776
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
777
|
+
connection = self._connection
|
|
778
|
+
self._connection = None
|
|
779
|
+
await self._close_connection_safely(connection, "service close")
|
|
758
780
|
|
|
759
781
|
self._active_consumer_tag = None
|
|
760
782
|
self._active_consumer_tags.clear()
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/consumer.py
RENAMED
|
@@ -601,22 +601,28 @@ class AsyncConsumerMixin(ABC):
|
|
|
601
601
|
# =====================================================================
|
|
602
602
|
# FETCH HANDLER (ASYNC)
|
|
603
603
|
# =====================================================================
|
|
604
|
-
async def _handle_fetch(
|
|
604
|
+
async def _handle_fetch(
|
|
605
|
+
self,
|
|
606
|
+
conn,
|
|
607
|
+
policy: policies.FetchPolicy,
|
|
608
|
+
batch_size: int | None = None,
|
|
609
|
+
) -> tuple[bool, List[connector.Transaction]]:
|
|
610
|
+
fetch_size = policy.batch.size if batch_size is None else batch_size
|
|
605
611
|
self._mark_consumer_progress("fetching")
|
|
606
|
-
logger.info(f"Fetch handler started for batch size {
|
|
612
|
+
logger.info(f"Fetch handler started for batch size {fetch_size}", extra=policy.extra)
|
|
607
613
|
fetch_start = time.perf_counter()
|
|
608
614
|
success, result = await helpers.run_fn_async(
|
|
609
|
-
fn=lambda: conn.fetch_transactions_async(
|
|
615
|
+
fn=lambda: conn.fetch_transactions_async(fetch_size, **policy.extra),
|
|
610
616
|
retry=policy.retry,
|
|
611
617
|
trace_name=f"{self.__class__.__name__}.fetch_transactions",
|
|
612
|
-
trace_attrs={"batch_size":
|
|
618
|
+
trace_attrs={"batch_size": fetch_size},
|
|
613
619
|
)
|
|
614
620
|
# Record fetch metrics
|
|
615
621
|
fetched_count = len(result) if success and result else 0
|
|
616
622
|
mixin_metrics.record_consumer_fetch(
|
|
617
623
|
task_name=getattr(self, "name", self.__class__.__name__),
|
|
618
624
|
connector_name=conn.__class__.__name__,
|
|
619
|
-
batch_size=
|
|
625
|
+
batch_size=fetch_size,
|
|
620
626
|
fetched_count=fetched_count,
|
|
621
627
|
duration=time.perf_counter() - fetch_start,
|
|
622
628
|
success=success,
|
|
@@ -877,13 +883,146 @@ class AsyncConsumerMixin(ABC):
|
|
|
877
883
|
)
|
|
878
884
|
return await self._handle_exception(tr, timeout_error, pol.exception.retry)
|
|
879
885
|
|
|
886
|
+
async def _consume_continuous():
|
|
887
|
+
nonlocal batch_number, empty_count, processed
|
|
888
|
+
|
|
889
|
+
concurrency = policy.loop.concurrency.value
|
|
890
|
+
submitted = 0
|
|
891
|
+
in_flight: set[asyncio.Task] = set()
|
|
892
|
+
|
|
893
|
+
async def reap(done: set[asyncio.Task]) -> None:
|
|
894
|
+
nonlocal processed
|
|
895
|
+
for task in done:
|
|
896
|
+
try:
|
|
897
|
+
await task
|
|
898
|
+
except Exception as exc:
|
|
899
|
+
logger.error("[async] Execution error: %s", exc)
|
|
900
|
+
finally:
|
|
901
|
+
processed += 1
|
|
902
|
+
|
|
903
|
+
async def wait_for_completion() -> None:
|
|
904
|
+
done, _ = await asyncio.wait(
|
|
905
|
+
in_flight,
|
|
906
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
907
|
+
)
|
|
908
|
+
in_flight.difference_update(done)
|
|
909
|
+
await reap(done)
|
|
910
|
+
self._mark_consumer_progress("polling")
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
while True:
|
|
914
|
+
if policy.loop.limit is not None and submitted >= policy.loop.limit:
|
|
915
|
+
if in_flight:
|
|
916
|
+
await wait_for_completion()
|
|
917
|
+
continue
|
|
918
|
+
break
|
|
919
|
+
|
|
920
|
+
free_slots = concurrency - len(in_flight)
|
|
921
|
+
if free_slots == 0:
|
|
922
|
+
await wait_for_completion()
|
|
923
|
+
continue
|
|
924
|
+
|
|
925
|
+
if policy.loop.limit is not None:
|
|
926
|
+
free_slots = min(free_slots, policy.loop.limit - submitted)
|
|
927
|
+
|
|
928
|
+
logger.debug(
|
|
929
|
+
"Fetching transactions for %d free continuous-consumer slot(s) with fetch policy: %s",
|
|
930
|
+
free_slots,
|
|
931
|
+
policy.fetch.model_dump_json(indent=2),
|
|
932
|
+
)
|
|
933
|
+
success, result = await self._handle_fetch(
|
|
934
|
+
conn,
|
|
935
|
+
policy.fetch,
|
|
936
|
+
batch_size=free_slots,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
if not success:
|
|
940
|
+
logger.error("Fetch failed → %s", result)
|
|
941
|
+
# Unlike shutdown/cancellation, a fetch failure must not
|
|
942
|
+
# cancel work already in progress: let in-flight
|
|
943
|
+
# transactions finish (bounded by the transaction
|
|
944
|
+
# runtime timeout) before propagating the failure.
|
|
945
|
+
if in_flight:
|
|
946
|
+
logger.warning(
|
|
947
|
+
"Fetch failed with %d transaction(s) in flight; draining before raising",
|
|
948
|
+
len(in_flight),
|
|
949
|
+
)
|
|
950
|
+
await asyncio.gather(*in_flight, return_exceptions=True)
|
|
951
|
+
in_flight.clear()
|
|
952
|
+
if isinstance(result, (asyncio.TimeoutError, futures.TimeoutError)):
|
|
953
|
+
raise exceptions.FetchTimeoutException(str(result))
|
|
954
|
+
raise exceptions.FetchException(str(result))
|
|
955
|
+
|
|
956
|
+
transactions = result
|
|
957
|
+
if transactions:
|
|
958
|
+
empty_count = 0
|
|
959
|
+
batch_number += 1
|
|
960
|
+
self._mark_consumer_progress("processing")
|
|
961
|
+
logger.info(
|
|
962
|
+
"%d transaction(s) fetched for continuous processing",
|
|
963
|
+
len(transactions),
|
|
964
|
+
)
|
|
965
|
+
mixin_metrics.record_consumer_batch(
|
|
966
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
967
|
+
batch_number=batch_number,
|
|
968
|
+
batch_size=len(transactions),
|
|
969
|
+
streaming=policy.loop.streaming,
|
|
970
|
+
)
|
|
971
|
+
for transaction in transactions:
|
|
972
|
+
in_flight.add(
|
|
973
|
+
asyncio.create_task(
|
|
974
|
+
submit_start_processing(transaction, policy),
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
submitted += 1
|
|
978
|
+
continue
|
|
979
|
+
|
|
980
|
+
if in_flight:
|
|
981
|
+
await wait_for_completion()
|
|
982
|
+
continue
|
|
983
|
+
|
|
984
|
+
self._mark_consumer_progress("idle")
|
|
985
|
+
logger.info("Empty fetch detected at %s", datetime.now().isoformat())
|
|
986
|
+
if not policy.loop.streaming:
|
|
987
|
+
logger.info("Non-streaming mode detected, breaking loop")
|
|
988
|
+
break
|
|
989
|
+
|
|
990
|
+
mixin_metrics.record_consumer_empty_queue_wait(
|
|
991
|
+
task_name=getattr(self, "name", self.__class__.__name__),
|
|
992
|
+
wait_count=empty_count,
|
|
993
|
+
)
|
|
994
|
+
await utils.backoff_async(
|
|
995
|
+
backoff=policy.fetch.empty.backoff,
|
|
996
|
+
multiplier=policy.fetch.empty.backoff_multiplier,
|
|
997
|
+
cap=policy.fetch.empty.backoff_cap,
|
|
998
|
+
attempt=empty_count,
|
|
999
|
+
)
|
|
1000
|
+
empty_count += 1
|
|
1001
|
+
except BaseException:
|
|
1002
|
+
for task in in_flight:
|
|
1003
|
+
task.cancel()
|
|
1004
|
+
if in_flight:
|
|
1005
|
+
await asyncio.gather(*in_flight, return_exceptions=True)
|
|
1006
|
+
raise
|
|
1007
|
+
|
|
1008
|
+
elapsed_time = time.perf_counter() - start_time
|
|
1009
|
+
logger.info(
|
|
1010
|
+
"[Consume continuous] Finished. Processed=%d in %.2f seconds",
|
|
1011
|
+
processed,
|
|
1012
|
+
elapsed_time,
|
|
1013
|
+
)
|
|
1014
|
+
return processed
|
|
1015
|
+
|
|
1016
|
+
if policy.loop.mode == "continuous":
|
|
1017
|
+
return await _consume_continuous()
|
|
1018
|
+
|
|
880
1019
|
while True:
|
|
881
1020
|
batch_number += 1
|
|
882
1021
|
|
|
883
1022
|
# ============================================================
|
|
884
1023
|
# FETCH
|
|
885
1024
|
# ============================================================
|
|
886
|
-
logger.
|
|
1025
|
+
logger.debug(f"Fetching transactions batch with fetch policy: {policy.fetch.model_dump_json(indent=2)}")
|
|
887
1026
|
success, result = await self._handle_fetch(conn, policy.fetch)
|
|
888
1027
|
|
|
889
1028
|
if not success:
|
|
@@ -941,7 +1080,7 @@ class AsyncConsumerMixin(ABC):
|
|
|
941
1080
|
else:
|
|
942
1081
|
batch_context = None # Use current context
|
|
943
1082
|
|
|
944
|
-
logger.
|
|
1083
|
+
logger.debug(
|
|
945
1084
|
f"Starting batch processing of "
|
|
946
1085
|
f"{len(transactions)} transaction(s) "
|
|
947
1086
|
f"from fetch handler with "
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional, Union
|
|
1
|
+
from typing import Literal, Optional, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field, field_validator
|
|
4
4
|
|
|
@@ -161,11 +161,20 @@ class ExceptionPolicy(BaseModel):
|
|
|
161
161
|
|
|
162
162
|
|
|
163
163
|
class ConsumerLoopPolicy(BaseModel):
|
|
164
|
+
mode: Literal["batch", "continuous"] = Field(default="batch")
|
|
164
165
|
concurrency: ConcurrencyPolicy = Field(default_factory=ConcurrencyPolicy)
|
|
165
166
|
timeout: Optional[float] = Field(default=None, ge=0)
|
|
166
167
|
limit: Optional[int] = Field(default=None, ge=0)
|
|
167
168
|
streaming: bool = Field(default=False)
|
|
168
169
|
|
|
170
|
+
@field_validator("mode", mode="before")
|
|
171
|
+
@classmethod
|
|
172
|
+
def _normalize_mode(cls, v):
|
|
173
|
+
v = _normalize_optional(v)
|
|
174
|
+
if v is None:
|
|
175
|
+
return "batch"
|
|
176
|
+
return v.strip().lower() if isinstance(v, str) else v
|
|
177
|
+
|
|
169
178
|
@field_validator("timeout", "limit", mode="before")
|
|
170
179
|
@classmethod
|
|
171
180
|
def _normalize_optional_numbers(cls, v):
|
|
@@ -106,15 +106,18 @@ class _TaskLivenessSupervisor:
|
|
|
106
106
|
self,
|
|
107
107
|
provider: LivenessProvider,
|
|
108
108
|
policy: TaskSupervisionPolicy,
|
|
109
|
-
request_shutdown,
|
|
110
|
-
hard_exit=
|
|
109
|
+
request_shutdown: Callable[[str], None],
|
|
110
|
+
hard_exit: Callable[[int], None] | None = None,
|
|
111
111
|
) -> None:
|
|
112
112
|
self.provider = provider
|
|
113
113
|
self.policy = policy
|
|
114
114
|
self.request_shutdown = request_shutdown
|
|
115
|
-
self.hard_exit = hard_exit
|
|
115
|
+
self.hard_exit = hard_exit or os._exit
|
|
116
116
|
self._stop = threading.Event()
|
|
117
117
|
self._thread: threading.Thread | None = None
|
|
118
|
+
self._shutdown_lock = threading.Lock()
|
|
119
|
+
self._shutdown_requested_at: float | None = None
|
|
120
|
+
self._shutdown_reason: str | None = None
|
|
118
121
|
|
|
119
122
|
def start(self) -> None:
|
|
120
123
|
self._thread = threading.Thread(
|
|
@@ -129,13 +132,39 @@ class _TaskLivenessSupervisor:
|
|
|
129
132
|
if self._thread is not None and self._thread is not threading.current_thread():
|
|
130
133
|
self._thread.join(timeout=max(1.0, self.policy.check_interval * 2))
|
|
131
134
|
|
|
135
|
+
def arm_shutdown_deadline(self, reason: str) -> None:
|
|
136
|
+
"""Force process exit if any cooperative shutdown or cleanup wedges."""
|
|
137
|
+
with self._shutdown_lock:
|
|
138
|
+
if self._shutdown_requested_at is not None:
|
|
139
|
+
return
|
|
140
|
+
self._shutdown_requested_at = time.monotonic()
|
|
141
|
+
self._shutdown_reason = reason
|
|
142
|
+
_supervisor_logger.warning(
|
|
143
|
+
"Task shutdown deadline armed for %.1fs (%s)",
|
|
144
|
+
self.policy.shutdown_grace,
|
|
145
|
+
reason,
|
|
146
|
+
)
|
|
147
|
+
|
|
132
148
|
def _run(self) -> None:
|
|
133
149
|
started_at = time.monotonic()
|
|
134
150
|
unhealthy_since: float | None = None
|
|
135
|
-
shutdown_requested_at: float | None = None
|
|
136
151
|
|
|
137
152
|
while not self._stop.wait(self.policy.check_interval):
|
|
138
153
|
now = time.monotonic()
|
|
154
|
+
with self._shutdown_lock:
|
|
155
|
+
shutdown_requested_at = self._shutdown_requested_at
|
|
156
|
+
shutdown_reason = self._shutdown_reason
|
|
157
|
+
if shutdown_requested_at is not None:
|
|
158
|
+
if now - shutdown_requested_at >= self.policy.shutdown_grace:
|
|
159
|
+
_supervisor_logger.critical(
|
|
160
|
+
"Task did not stop within %.1fs after shutdown began (%s); forcing exit",
|
|
161
|
+
self.policy.shutdown_grace,
|
|
162
|
+
shutdown_reason,
|
|
163
|
+
)
|
|
164
|
+
self._stop.set()
|
|
165
|
+
self.hard_exit(int(ExitCode.ERROR))
|
|
166
|
+
continue
|
|
167
|
+
|
|
139
168
|
if now - started_at < self.policy.startup_grace:
|
|
140
169
|
continue
|
|
141
170
|
|
|
@@ -157,22 +186,14 @@ class _TaskLivenessSupervisor:
|
|
|
157
186
|
_supervisor_logger.error("Task liveness check failed: %s", reason)
|
|
158
187
|
continue
|
|
159
188
|
|
|
160
|
-
if
|
|
161
|
-
shutdown_requested_at = now
|
|
189
|
+
if now - unhealthy_since >= self.policy.unhealthy_grace:
|
|
162
190
|
_supervisor_logger.critical(
|
|
163
191
|
"Task remained unhealthy for %.1fs (%s); requesting process shutdown",
|
|
164
192
|
now - unhealthy_since,
|
|
165
193
|
reason,
|
|
166
194
|
)
|
|
195
|
+
self.arm_shutdown_deadline(reason)
|
|
167
196
|
self.request_shutdown(reason)
|
|
168
|
-
continue
|
|
169
|
-
|
|
170
|
-
if shutdown_requested_at is not None and now - shutdown_requested_at >= self.policy.shutdown_grace:
|
|
171
|
-
_supervisor_logger.critical(
|
|
172
|
-
"Task did not stop within %.1fs after liveness shutdown request; forcing exit",
|
|
173
|
-
self.policy.shutdown_grace,
|
|
174
|
-
)
|
|
175
|
-
self.hard_exit(int(ExitCode.ERROR))
|
|
176
197
|
|
|
177
198
|
|
|
178
199
|
# =============================================================
|
|
@@ -313,8 +334,10 @@ async def __run_task_async(
|
|
|
313
334
|
raise
|
|
314
335
|
finally:
|
|
315
336
|
_unregister_shutdown_callback(request_signal_shutdown)
|
|
316
|
-
if supervisor is not None
|
|
317
|
-
supervisor.
|
|
337
|
+
if supervisor is not None:
|
|
338
|
+
supervisor.arm_shutdown_deadline(
|
|
339
|
+
"liveness shutdown cleanup" if liveness_shutdown_requested.is_set() else "task cleanup"
|
|
340
|
+
)
|
|
318
341
|
try:
|
|
319
342
|
if instance is not None:
|
|
320
343
|
await instance.exit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/connector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/connector.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/connector.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/utils.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/version.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/helper.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/connector.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/connector/transaction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/metrics.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/producer.py
RENAMED
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/_resource.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/logging.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/metrics.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7 → ergon_framework_python-0.1.9}/src/ergon/telemetry/tracing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|