ergon-framework-python 0.3.0__tar.gz → 0.3.1__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.3.0/src/ergon_framework_python.egg-info → ergon_framework_python-0.3.1}/PKG-INFO +1 -1
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/pyproject.toml +1 -1
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/__init__.py +1 -1
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/connector.py +4 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/connector.py +7 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/connector.py +5 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/connector.py +5 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/helpers.py +20 -1
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/consumer.py +71 -2
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/runner.py +114 -70
- ergon_framework_python-0.3.1/src/ergon/task/shutdown.py +275 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/utils.py +14 -3
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1/src/ergon_framework_python.egg-info}/PKG-INFO +1 -1
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/SOURCES.txt +1 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/tests/test_smoke.py +1 -1
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/LICENSE +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/README.md +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/setup.cfg +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/main.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/cli.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/_lazy.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/_client.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/adapters.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/activity.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/addresses.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/attachments.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/messages.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/records.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/utils.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/_mode.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/_operations.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/utils.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/async_auth_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/async_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/auth_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/utils.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/async_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/utils.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/async_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/version.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/async_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/async_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/helper.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/async_connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/async_service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/connector.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/models.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/transaction.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/py.typed +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/service/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/service/service.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/base.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/exceptions.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/liveness.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/manager.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/metrics.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/producer.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/policies.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/telemetry/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/telemetry/_resource.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/telemetry/logging.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/telemetry/metrics.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/telemetry/tracing.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/utils/__init__.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/utils/env.py +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/requires.txt +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
- {ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/tests/test_optional_connectors.py +0 -0
|
@@ -208,6 +208,10 @@ class ErgonPlatformChannelsConnector(Connector):
|
|
|
208
208
|
if requeue and transaction.id:
|
|
209
209
|
self._seen_event_ids.discard(transaction.id)
|
|
210
210
|
|
|
211
|
+
def release_transaction(self, transaction: Transaction) -> None:
|
|
212
|
+
"""Give back a fetched but unstarted inbox event (worker shutdown): requeued with no delay."""
|
|
213
|
+
self.nack_transaction(transaction, requeue=True, delay_seconds=0)
|
|
214
|
+
|
|
211
215
|
def download_attachments(
|
|
212
216
|
self,
|
|
213
217
|
transaction: Transaction,
|
|
@@ -159,6 +159,13 @@ class ErgonPlatformConnector(Connector):
|
|
|
159
159
|
**fields,
|
|
160
160
|
)
|
|
161
161
|
|
|
162
|
+
def release_transaction(self, transaction: Transaction) -> None:
|
|
163
|
+
"""Give back a transaction that was fetched but never started (worker shutdown).
|
|
164
|
+
|
|
165
|
+
A plain release: no failure reason, no cooldown, so it is picked up again right away.
|
|
166
|
+
"""
|
|
167
|
+
self.release_item(transaction.id)
|
|
168
|
+
|
|
162
169
|
def nack_transaction(
|
|
163
170
|
self,
|
|
164
171
|
transaction: Transaction,
|
{ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/connector.py
RENAMED
|
@@ -119,5 +119,10 @@ class NylasConnector(Connector):
|
|
|
119
119
|
return
|
|
120
120
|
logger.debug("nack_transaction is a no-op for Nylas; message %s remains available for refetch", transaction.id)
|
|
121
121
|
|
|
122
|
+
def release_transaction(self, transaction: Transaction) -> None:
|
|
123
|
+
"""Give back a fetched but unstarted message (worker shutdown): unchanged, refetchable."""
|
|
124
|
+
self.service.reset_pagination()
|
|
125
|
+
logger.debug("Nylas message %s released unchanged", transaction.id)
|
|
126
|
+
|
|
122
127
|
def close(self) -> None:
|
|
123
128
|
self.service.close()
|
|
@@ -194,5 +194,10 @@ class OutlookGraphConnector(Connector):
|
|
|
194
194
|
self.service.reset_pagination()
|
|
195
195
|
logger.debug("Outlook message %s was made available for refetch", transaction.id)
|
|
196
196
|
|
|
197
|
+
def release_transaction(self, transaction: Transaction) -> None:
|
|
198
|
+
"""Give back a fetched but unstarted message (worker shutdown): unchanged, refetchable."""
|
|
199
|
+
self.service.reset_pagination()
|
|
200
|
+
logger.debug("Outlook message %s released unchanged", transaction.id)
|
|
201
|
+
|
|
197
202
|
def close(self) -> None:
|
|
198
203
|
self.service.close()
|
|
@@ -309,12 +309,17 @@ def run_fn(
|
|
|
309
309
|
return run()
|
|
310
310
|
|
|
311
311
|
|
|
312
|
+
_STOP_POLL_SECONDS = 0.25
|
|
313
|
+
|
|
314
|
+
|
|
312
315
|
def multithread_execute(
|
|
313
316
|
submissions: Iterable[Callable[[], futures.Future]],
|
|
314
317
|
*,
|
|
315
318
|
concurrency: int,
|
|
316
319
|
limit: Optional[int] = None,
|
|
317
320
|
timeout: Optional[float] = None,
|
|
321
|
+
stop: Optional[Callable[[], bool]] = None,
|
|
322
|
+
on_stop: Optional[Callable[[], None]] = None,
|
|
318
323
|
) -> int:
|
|
319
324
|
"""
|
|
320
325
|
Execute submitted callables with bounded concurrency and refill.
|
|
@@ -322,6 +327,8 @@ def multithread_execute(
|
|
|
322
327
|
- submissions yields callables that RETURN a Future when called
|
|
323
328
|
- this function manages in-flight futures only
|
|
324
329
|
- no domain semantics, no retries, no tracing
|
|
330
|
+
- ``stop`` is polled while futures are in flight; once it returns True nothing more is
|
|
331
|
+
submitted, ``on_stop`` runs once right away, and the in-flight futures are still awaited
|
|
325
332
|
|
|
326
333
|
Returns:
|
|
327
334
|
Number of completed submissions
|
|
@@ -329,6 +336,10 @@ def multithread_execute(
|
|
|
329
336
|
|
|
330
337
|
in_flight: dict[futures.Future, float] = {}
|
|
331
338
|
completed = 0
|
|
339
|
+
stopping = False
|
|
340
|
+
wait_timeout = timeout
|
|
341
|
+
if stop is not None:
|
|
342
|
+
wait_timeout = _STOP_POLL_SECONDS if timeout is None else min(timeout, _STOP_POLL_SECONDS)
|
|
332
343
|
|
|
333
344
|
submit_iter = iter(submissions)
|
|
334
345
|
|
|
@@ -363,7 +374,7 @@ def multithread_execute(
|
|
|
363
374
|
done, _ = futures.wait(
|
|
364
375
|
in_flight.keys(),
|
|
365
376
|
return_when=futures.FIRST_COMPLETED,
|
|
366
|
-
timeout=
|
|
377
|
+
timeout=wait_timeout,
|
|
367
378
|
)
|
|
368
379
|
|
|
369
380
|
# ----------------------------
|
|
@@ -391,6 +402,14 @@ def multithread_execute(
|
|
|
391
402
|
in_flight.pop(fut)
|
|
392
403
|
completed += 1
|
|
393
404
|
|
|
405
|
+
if not stopping and stop is not None and stop():
|
|
406
|
+
stopping = True
|
|
407
|
+
logger.info("Stop requested; waiting for %d in-flight submission(s), submitting no more", len(in_flight))
|
|
408
|
+
if on_stop is not None:
|
|
409
|
+
on_stop()
|
|
410
|
+
if stopping:
|
|
411
|
+
continue
|
|
412
|
+
|
|
394
413
|
# ============================================================
|
|
395
414
|
# REFILL
|
|
396
415
|
# ============================================================
|
{ergon_framework_python-0.3.0 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/consumer.py
RENAMED
|
@@ -10,7 +10,7 @@ from async_timeout import timeout as timeout_after
|
|
|
10
10
|
from opentelemetry import context as otel_context
|
|
11
11
|
|
|
12
12
|
from ... import connector, telemetry
|
|
13
|
-
from .. import base, exceptions, helpers, liveness, policies, utils
|
|
13
|
+
from .. import base, exceptions, helpers, liveness, policies, shutdown, utils
|
|
14
14
|
from . import metrics as mixin_metrics
|
|
15
15
|
from . import producer
|
|
16
16
|
|
|
@@ -304,6 +304,38 @@ class ConsumerMixin(ABC):
|
|
|
304
304
|
return next(iter(self.connectors.values()))
|
|
305
305
|
raise ValueError("Multiple connectors configured; specify one in policy")
|
|
306
306
|
|
|
307
|
+
# =====================================================================
|
|
308
|
+
# SHUTDOWN HAND-BACK
|
|
309
|
+
# =====================================================================
|
|
310
|
+
def _hand_back_unstarted(self, conn, transactions: List[connector.Transaction]) -> None:
|
|
311
|
+
"""Give back fetched transactions that were never started because the process is stopping.
|
|
312
|
+
|
|
313
|
+
``release_transaction`` (no verdict) when the connector has it, else
|
|
314
|
+
``nack_transaction(requeue=True)``. Never raises: the stop must go on, and a
|
|
315
|
+
transaction that cannot be handed back returns when its claim or lease expires.
|
|
316
|
+
"""
|
|
317
|
+
release = getattr(conn, "release_transaction", None)
|
|
318
|
+
nack = getattr(conn, "nack_transaction", None)
|
|
319
|
+
for tr in transactions:
|
|
320
|
+
try:
|
|
321
|
+
if callable(release):
|
|
322
|
+
release(tr)
|
|
323
|
+
elif callable(nack):
|
|
324
|
+
nack(tr, requeue=True)
|
|
325
|
+
else:
|
|
326
|
+
logger.warning(
|
|
327
|
+
"Connector %s cannot hand back transaction %s; it returns when its claim or lease expires",
|
|
328
|
+
conn.__class__.__name__,
|
|
329
|
+
tr.id,
|
|
330
|
+
)
|
|
331
|
+
continue
|
|
332
|
+
logger.info("Handed back unstarted transaction %s", tr.id)
|
|
333
|
+
except Exception:
|
|
334
|
+
logger.exception(
|
|
335
|
+
"Could not hand back transaction %s; it returns when its claim or lease expires",
|
|
336
|
+
tr.id,
|
|
337
|
+
)
|
|
338
|
+
|
|
307
339
|
# =====================================================================
|
|
308
340
|
# PUBLIC CONSUME LOOP
|
|
309
341
|
# =====================================================================
|
|
@@ -338,6 +370,10 @@ class ConsumerMixin(ABC):
|
|
|
338
370
|
)
|
|
339
371
|
|
|
340
372
|
while True:
|
|
373
|
+
if shutdown.is_requested():
|
|
374
|
+
logger.info("Shutdown requested; consume loop stops before the next fetch")
|
|
375
|
+
break
|
|
376
|
+
|
|
341
377
|
batch_number += 1
|
|
342
378
|
|
|
343
379
|
# -------------------------
|
|
@@ -345,6 +381,9 @@ class ConsumerMixin(ABC):
|
|
|
345
381
|
# -------------------------
|
|
346
382
|
logger.debug(f"Fetching transactions batch with fetch policy: {policy.fetch.model_dump_json(indent=2)}")
|
|
347
383
|
success, result = self._handle_fetch(conn, policy.fetch)
|
|
384
|
+
if not success and shutdown.is_requested():
|
|
385
|
+
logger.warning(f"Fetch failed while stopping; nothing was fetched, stopping → {result}")
|
|
386
|
+
break
|
|
348
387
|
if not success:
|
|
349
388
|
logger.error(f"Fetch failed → {result}")
|
|
350
389
|
executor.shutdown(wait=False)
|
|
@@ -354,6 +393,14 @@ class ConsumerMixin(ABC):
|
|
|
354
393
|
|
|
355
394
|
transactions = result
|
|
356
395
|
|
|
396
|
+
if transactions and shutdown.is_requested():
|
|
397
|
+
logger.info(
|
|
398
|
+
"Shutdown requested during fetch; handing back %d unstarted transaction(s)",
|
|
399
|
+
len(transactions),
|
|
400
|
+
)
|
|
401
|
+
self._hand_back_unstarted(conn, list(transactions))
|
|
402
|
+
break
|
|
403
|
+
|
|
357
404
|
# -------------------------
|
|
358
405
|
# EMPTY QUEUE HANDLING
|
|
359
406
|
# -------------------------
|
|
@@ -375,6 +422,7 @@ class ConsumerMixin(ABC):
|
|
|
375
422
|
policy.fetch.empty.backoff_multiplier,
|
|
376
423
|
policy.fetch.empty.backoff_cap,
|
|
377
424
|
empty_count,
|
|
425
|
+
interruptible=True,
|
|
378
426
|
)
|
|
379
427
|
empty_count += 1
|
|
380
428
|
continue
|
|
@@ -417,11 +465,24 @@ class ConsumerMixin(ABC):
|
|
|
417
465
|
"streaming": policy.loop.streaming,
|
|
418
466
|
},
|
|
419
467
|
):
|
|
468
|
+
not_started = list(transactions)
|
|
420
469
|
|
|
421
470
|
def submissions():
|
|
422
|
-
|
|
471
|
+
while not_started and not shutdown.is_requested():
|
|
472
|
+
tr = not_started.pop(0)
|
|
423
473
|
yield lambda tr=tr: submit_start_processing(tr, policy)
|
|
424
474
|
|
|
475
|
+
def hand_back_not_started():
|
|
476
|
+
remaining = list(not_started)
|
|
477
|
+
not_started.clear()
|
|
478
|
+
if remaining:
|
|
479
|
+
logger.info(
|
|
480
|
+
"Shutdown requested; handing back %d unstarted transaction(s) of this batch, "
|
|
481
|
+
"in-flight ones will finish",
|
|
482
|
+
len(remaining),
|
|
483
|
+
)
|
|
484
|
+
self._hand_back_unstarted(conn, remaining)
|
|
485
|
+
|
|
425
486
|
logger.debug(
|
|
426
487
|
f"Submitting {len(transactions)} transactions for processing "
|
|
427
488
|
f"with concurrency policy: {policy.loop.concurrency.model_dump_json(indent=2)}."
|
|
@@ -432,10 +493,17 @@ class ConsumerMixin(ABC):
|
|
|
432
493
|
concurrency=policy.loop.concurrency.value,
|
|
433
494
|
limit=policy.loop.limit,
|
|
434
495
|
timeout=policy.transaction_runtime.timeout,
|
|
496
|
+
stop=shutdown.is_requested,
|
|
497
|
+
on_stop=hand_back_not_started,
|
|
435
498
|
)
|
|
436
499
|
|
|
437
500
|
processed += count
|
|
438
501
|
|
|
502
|
+
if shutdown.is_requested():
|
|
503
|
+
hand_back_not_started()
|
|
504
|
+
logger.info("Shutdown requested; consume loop stops after the in-flight transactions")
|
|
505
|
+
break
|
|
506
|
+
|
|
439
507
|
if policy.loop.limit and processed >= policy.loop.limit:
|
|
440
508
|
break
|
|
441
509
|
|
|
@@ -446,6 +514,7 @@ class ConsumerMixin(ABC):
|
|
|
446
514
|
multiplier=policy.fetch.batch.interval.backoff_multiplier,
|
|
447
515
|
cap=policy.fetch.batch.interval.backoff_cap,
|
|
448
516
|
attempt=0,
|
|
517
|
+
interruptible=True,
|
|
449
518
|
)
|
|
450
519
|
|
|
451
520
|
executor.shutdown()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import inspect
|
|
3
3
|
import logging as stdlib_logging
|
|
4
|
+
import multiprocessing
|
|
5
|
+
import multiprocessing.connection
|
|
4
6
|
import os
|
|
5
7
|
import signal
|
|
6
8
|
import threading
|
|
@@ -8,13 +10,13 @@ import time
|
|
|
8
10
|
import traceback
|
|
9
11
|
import uuid
|
|
10
12
|
from collections.abc import Callable
|
|
11
|
-
from concurrent.futures import ProcessPoolExecutor
|
|
12
13
|
from datetime import datetime
|
|
13
14
|
from enum import IntEnum
|
|
14
15
|
from typing import Any, Literal
|
|
15
16
|
|
|
16
17
|
from ..connector import Transaction
|
|
17
18
|
from ..telemetry import logging, metrics, tracing
|
|
19
|
+
from . import shutdown
|
|
18
20
|
from .base import (
|
|
19
21
|
BaseAsyncTask,
|
|
20
22
|
BaseTask,
|
|
@@ -38,63 +40,24 @@ class ExitCode(IntEnum):
|
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
# =============================================================
|
|
41
|
-
# SHUTDOWN STATE (PROCESS-LOCAL)
|
|
43
|
+
# SHUTDOWN STATE (PROCESS-LOCAL, see ``shutdown``)
|
|
42
44
|
# =============================================================
|
|
43
45
|
|
|
44
|
-
_shutdown_event = threading.Event()
|
|
45
|
-
_shutdown_signal: int | None = None
|
|
46
|
-
_shutdown_callbacks: set[Callable[[], None]] = set()
|
|
47
|
-
_shutdown_callbacks_lock = threading.Lock()
|
|
48
46
|
_supervisor_logger = stdlib_logging.getLogger(__name__)
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
callbacks = tuple(_shutdown_callbacks)
|
|
57
|
-
for callback in callbacks:
|
|
58
|
-
try:
|
|
59
|
-
callback()
|
|
60
|
-
except Exception:
|
|
61
|
-
_supervisor_logger.exception("Async shutdown callback failed")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _install_signal_handlers():
|
|
65
|
-
signal.signal(signal.SIGINT, _signal_handler)
|
|
66
|
-
signal.signal(signal.SIGTERM, _signal_handler)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _reset_shutdown_state() -> None:
|
|
70
|
-
global _shutdown_signal
|
|
71
|
-
_shutdown_event.clear()
|
|
72
|
-
_shutdown_signal = None
|
|
73
|
-
with _shutdown_callbacks_lock:
|
|
74
|
-
_shutdown_callbacks.clear()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _register_shutdown_callback(callback: Callable[[], None]) -> None:
|
|
78
|
-
with _shutdown_callbacks_lock:
|
|
79
|
-
_shutdown_callbacks.add(callback)
|
|
80
|
-
shutdown_requested = _shutdown_event.is_set()
|
|
81
|
-
if shutdown_requested:
|
|
82
|
-
callback()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _unregister_shutdown_callback(callback: Callable[[], None]) -> None:
|
|
86
|
-
with _shutdown_callbacks_lock:
|
|
87
|
-
_shutdown_callbacks.discard(callback)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def is_shutdown_requested() -> bool:
|
|
91
|
-
return _shutdown_event.is_set()
|
|
48
|
+
_signal_handler = shutdown._handle_signal
|
|
49
|
+
_install_signal_handlers = shutdown.install_handlers
|
|
50
|
+
_reset_shutdown_state = shutdown.reset
|
|
51
|
+
_register_shutdown_callback = shutdown.register_callback
|
|
52
|
+
_unregister_shutdown_callback = shutdown.unregister_callback
|
|
53
|
+
is_shutdown_requested = shutdown.is_requested
|
|
92
54
|
|
|
93
55
|
|
|
94
56
|
def get_shutdown_exit_code() -> ExitCode:
|
|
95
|
-
|
|
57
|
+
requested = shutdown.requested_signal()
|
|
58
|
+
if requested == signal.SIGINT:
|
|
96
59
|
return ExitCode.SIGINT
|
|
97
|
-
if
|
|
60
|
+
if requested == signal.SIGTERM:
|
|
98
61
|
return ExitCode.SIGTERM
|
|
99
62
|
return ExitCode.ERROR
|
|
100
63
|
|
|
@@ -497,6 +460,103 @@ def __run_task_sync(
|
|
|
497
460
|
instance.exit()
|
|
498
461
|
|
|
499
462
|
|
|
463
|
+
def _run_worker_sync(config: TaskConfig, mode: Literal["task", "transaction"] = "task", *args, **kwargs):
|
|
464
|
+
"""Worker-process entrypoint: cooperative handlers, no escalation, keeps an early stop request."""
|
|
465
|
+
shutdown.prepare_worker()
|
|
466
|
+
if shutdown.is_requested():
|
|
467
|
+
_supervisor_logger.warning("Stop requested before worker %s started; not running it", kwargs.get("worker_id"))
|
|
468
|
+
return None
|
|
469
|
+
return __run_task_sync(config, mode, *args, **kwargs)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _worker_signal_relays(worker_pids: Callable[[], list[int]]) -> tuple[Callable[[], None], Callable[[], None]]:
|
|
473
|
+
"""(forward the parent's signal to the workers, SIGKILL the workers).
|
|
474
|
+
|
|
475
|
+
``worker_pids`` is read on every call, so workers started after registration count.
|
|
476
|
+
``kill_workers`` runs right before the process exits, so it does not log.
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
def _send(signum: int) -> list[tuple[int, OSError]]:
|
|
480
|
+
failures = []
|
|
481
|
+
for pid in worker_pids():
|
|
482
|
+
try:
|
|
483
|
+
os.kill(pid, signum)
|
|
484
|
+
except ProcessLookupError:
|
|
485
|
+
continue
|
|
486
|
+
except OSError as exc:
|
|
487
|
+
failures.append((pid, exc))
|
|
488
|
+
return failures
|
|
489
|
+
|
|
490
|
+
def forward_shutdown() -> None:
|
|
491
|
+
for pid, exc in _send(shutdown.requested_signal() or signal.SIGTERM):
|
|
492
|
+
_supervisor_logger.error("Could not signal worker process %s: %s", pid, exc)
|
|
493
|
+
|
|
494
|
+
def kill_workers() -> None:
|
|
495
|
+
_send(signal.SIGKILL)
|
|
496
|
+
|
|
497
|
+
return forward_shutdown, kill_workers
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _start_worker(config: TaskConfig, mode: Literal["task", "transaction"], args: tuple, worker_kwargs: dict):
|
|
501
|
+
process = multiprocessing.Process(
|
|
502
|
+
target=_run_worker_sync,
|
|
503
|
+
args=(config, mode, *args),
|
|
504
|
+
kwargs=worker_kwargs,
|
|
505
|
+
name=f"{config.name}-worker-{worker_kwargs['worker_id']}",
|
|
506
|
+
)
|
|
507
|
+
process.start()
|
|
508
|
+
return process
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _run_workers(config: TaskConfig, mode: Literal["task", "transaction"], args: tuple, kwargs: dict) -> bool:
|
|
512
|
+
"""Run ``max_workers`` worker processes to completion; True when one of them failed.
|
|
513
|
+
|
|
514
|
+
Plain processes, not a pool: a worker whose task returns simply exits. A worker that
|
|
515
|
+
fails or dies stops its siblings cooperatively, as a broken pool used to.
|
|
516
|
+
"""
|
|
517
|
+
processes: list[multiprocessing.Process] = []
|
|
518
|
+
has_error = False
|
|
519
|
+
|
|
520
|
+
def alive_pids() -> list[int]:
|
|
521
|
+
return [p.pid for p in processes if p.pid is not None and p.exitcode is None]
|
|
522
|
+
|
|
523
|
+
forward_shutdown, kill_workers = _worker_signal_relays(alive_pids)
|
|
524
|
+
shutdown.register_callback(forward_shutdown)
|
|
525
|
+
shutdown.register_escalation_callback(kill_workers)
|
|
526
|
+
try:
|
|
527
|
+
# Workers inherit the blocked mask, so a signal that arrives while they start
|
|
528
|
+
# stays pending until ``prepare_worker`` installed their handlers.
|
|
529
|
+
with shutdown.blocked_signals():
|
|
530
|
+
for worker_id in range(config.max_workers):
|
|
531
|
+
if is_shutdown_requested():
|
|
532
|
+
break
|
|
533
|
+
worker_kwargs = {**kwargs, "worker_id": worker_id, "total_workers": config.max_workers}
|
|
534
|
+
processes.append(_start_worker(config, mode, args, worker_kwargs))
|
|
535
|
+
if is_shutdown_requested():
|
|
536
|
+
forward_shutdown()
|
|
537
|
+
|
|
538
|
+
running = list(processes)
|
|
539
|
+
while running:
|
|
540
|
+
multiprocessing.connection.wait([p.sentinel for p in running], timeout=1.0)
|
|
541
|
+
for p in [p for p in running if p.exitcode is not None]:
|
|
542
|
+
running.remove(p)
|
|
543
|
+
p.join()
|
|
544
|
+
if p.exitcode == 0:
|
|
545
|
+
continue
|
|
546
|
+
has_error = True
|
|
547
|
+
_supervisor_logger.error("Worker process %s (pid %s) exited with %s", p.name, p.pid, p.exitcode)
|
|
548
|
+
for sibling in running:
|
|
549
|
+
if sibling.pid is not None:
|
|
550
|
+
try:
|
|
551
|
+
os.kill(sibling.pid, signal.SIGTERM)
|
|
552
|
+
except ProcessLookupError:
|
|
553
|
+
pass
|
|
554
|
+
finally:
|
|
555
|
+
shutdown.unregister_callback(forward_shutdown)
|
|
556
|
+
shutdown.unregister_escalation_callback(kill_workers)
|
|
557
|
+
return has_error
|
|
558
|
+
|
|
559
|
+
|
|
500
560
|
# =============================================================
|
|
501
561
|
# PUBLIC API — RUNNER
|
|
502
562
|
# =============================================================
|
|
@@ -515,9 +575,10 @@ def run_task(
|
|
|
515
575
|
Returns POSIX-compatible exit code.
|
|
516
576
|
"""
|
|
517
577
|
|
|
518
|
-
_reset_shutdown_state()
|
|
519
|
-
_install_signal_handlers()
|
|
520
578
|
is_async = issubclass(config.task, BaseAsyncTask) # type: ignore[arg-type]
|
|
579
|
+
_reset_shutdown_state()
|
|
580
|
+
# Async: every signal re-cancels and the liveness supervisor owns the hard deadline.
|
|
581
|
+
_install_signal_handlers(escalate=not is_async)
|
|
521
582
|
|
|
522
583
|
# ---------------------------------------------------------
|
|
523
584
|
# SINGLE PROCESS
|
|
@@ -552,24 +613,7 @@ def run_task(
|
|
|
552
613
|
if is_async:
|
|
553
614
|
raise RuntimeError("Async tasks cannot be executed with multiple processes. Use debug=True or max_workers=1.")
|
|
554
615
|
|
|
555
|
-
has_error =
|
|
556
|
-
|
|
557
|
-
with ProcessPoolExecutor(max_workers=config.max_workers) as executor:
|
|
558
|
-
futures = []
|
|
559
|
-
for worker_id in range(config.max_workers):
|
|
560
|
-
worker_kwargs = {
|
|
561
|
-
**kwargs,
|
|
562
|
-
"worker_id": worker_id,
|
|
563
|
-
"total_workers": config.max_workers,
|
|
564
|
-
}
|
|
565
|
-
futures.append(executor.submit(__run_task_sync, config, mode, *args, **worker_kwargs))
|
|
566
|
-
|
|
567
|
-
for f in futures:
|
|
568
|
-
try:
|
|
569
|
-
f.result()
|
|
570
|
-
except Exception:
|
|
571
|
-
traceback.print_exc()
|
|
572
|
-
has_error = True
|
|
616
|
+
has_error = _run_workers(config, mode, args, kwargs)
|
|
573
617
|
|
|
574
618
|
if is_shutdown_requested():
|
|
575
619
|
return int(get_shutdown_exit_code())
|