ergon-framework-python 0.1.5__py3-none-any.whl → 0.1.6__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 CHANGED
@@ -10,4 +10,4 @@ __all__ = [
10
10
  "manager",
11
11
  ]
12
12
 
13
- __version__ = "0.1.5"
13
+ __version__ = "0.1.6"
ergon/task/runner.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import inspect
2
3
  import logging as stdlib_logging
3
4
  import os
4
5
  import signal
@@ -6,6 +7,7 @@ import threading
6
7
  import time
7
8
  import traceback
8
9
  import uuid
10
+ from collections.abc import Callable
9
11
  from concurrent.futures import ProcessPoolExecutor
10
12
  from datetime import datetime
11
13
  from enum import IntEnum
@@ -41,6 +43,8 @@ class ExitCode(IntEnum):
41
43
 
42
44
  _shutdown_event = threading.Event()
43
45
  _shutdown_signal: int | None = None
46
+ _shutdown_callbacks: set[Callable[[], None]] = set()
47
+ _shutdown_callbacks_lock = threading.Lock()
44
48
  _supervisor_logger = stdlib_logging.getLogger(__name__)
45
49
 
46
50
 
@@ -48,6 +52,13 @@ def _signal_handler(signum, frame):
48
52
  global _shutdown_signal
49
53
  _shutdown_signal = signum
50
54
  _shutdown_event.set()
55
+ with _shutdown_callbacks_lock:
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")
51
62
 
52
63
 
53
64
  def _install_signal_handlers():
@@ -55,6 +66,27 @@ def _install_signal_handlers():
55
66
  signal.signal(signal.SIGTERM, _signal_handler)
56
67
 
57
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
+
58
90
  def is_shutdown_requested() -> bool:
59
91
  return _shutdown_event.is_set()
60
92
 
@@ -216,20 +248,31 @@ async def __run_task_async(
216
248
 
217
249
  instance = None
218
250
  supervisor = None
251
+ liveness_shutdown_requested = threading.Event()
252
+ connectors: dict[str, Any] = {}
253
+ services: dict[str, Any] = {}
254
+ loop = asyncio.get_running_loop()
255
+ runner_task = asyncio.current_task()
256
+
257
+ def request_signal_shutdown() -> None:
258
+ if runner_task is not None and not runner_task.done():
259
+ loop.call_soon_threadsafe(runner_task.cancel, "Process shutdown requested")
260
+
261
+ _register_shutdown_callback(request_signal_shutdown)
219
262
 
220
263
  try:
221
264
  with tracer.start_as_current_span( # type: ignore[attr-defined]
222
265
  f"{config.task.__name__}.run",
223
266
  attributes={"task.execution.id": task_exec_metadata["execution_id"]},
224
267
  ):
225
- connectors = {}
226
268
  for name, cfg in config.connectors.items():
227
269
  conn = cfg.connector(*cfg.args, **cfg.kwargs)
270
+ connectors[name] = conn
228
271
  if hasattr(conn, "init_async"):
229
272
  await conn.init_async() # type: ignore[attr-defined]
230
- connectors[name] = conn
231
273
 
232
- services = {name: cfg.service(*cfg.args, **cfg.kwargs) for name, cfg in config.services.items()}
274
+ for name, cfg in config.services.items():
275
+ services[name] = cfg.service(*cfg.args, **cfg.kwargs)
233
276
 
234
277
  instance = config.task(
235
278
  connectors=connectors,
@@ -242,10 +285,9 @@ async def __run_task_async(
242
285
  )
243
286
 
244
287
  if config.supervision.enabled and isinstance(instance, LivenessProvider):
245
- loop = asyncio.get_running_loop()
246
- runner_task = asyncio.current_task()
247
288
 
248
289
  def request_liveness_shutdown(reason: str) -> None:
290
+ liveness_shutdown_requested.set()
249
291
  if runner_task is not None and not runner_task.done():
250
292
  loop.call_soon_threadsafe(runner_task.cancel, f"Task unhealthy: {reason}")
251
293
 
@@ -266,11 +308,37 @@ async def __run_task_async(
266
308
  else:
267
309
  await instance.execute()
268
310
 
311
+ except asyncio.CancelledError:
312
+ if not is_shutdown_requested():
313
+ raise
269
314
  finally:
270
- if supervisor is not None:
315
+ _unregister_shutdown_callback(request_signal_shutdown)
316
+ if supervisor is not None and not liveness_shutdown_requested.is_set():
271
317
  supervisor.stop()
272
- if instance is not None:
273
- await instance.exit()
318
+ try:
319
+ if instance is not None:
320
+ await instance.exit()
321
+ finally:
322
+ try:
323
+ resources = [*connectors.values(), *services.values()]
324
+ for resource in reversed(resources):
325
+ close = getattr(resource, "close", None)
326
+ if close is None:
327
+ close = getattr(resource, "aclose", None)
328
+ if not callable(close):
329
+ continue
330
+ try:
331
+ result = close()
332
+ if inspect.isawaitable(result):
333
+ await result
334
+ except Exception:
335
+ _supervisor_logger.exception(
336
+ "Failed to close async task resource %s",
337
+ type(resource).__name__,
338
+ )
339
+ finally:
340
+ if supervisor is not None:
341
+ supervisor.stop()
274
342
 
275
343
 
276
344
  # =============================================================
@@ -424,6 +492,7 @@ def run_task(
424
492
  Returns POSIX-compatible exit code.
425
493
  """
426
494
 
495
+ _reset_shutdown_state()
427
496
  _install_signal_handlers()
428
497
  is_async = issubclass(config.task, BaseAsyncTask) # type: ignore[arg-type]
429
498
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ergon-framework-python
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Ergon internal task-oriented project bootstrapper
5
5
  Author-email: Ergondata Technologies <anza.vossos@protonmail.com>
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- ergon/__init__.py,sha256=no__VCHvRMOkqda_d5YDcfQZ8oWnDWYMNXm3VddDf0o,218
1
+ ergon/__init__.py,sha256=dFNLM2h6C9VhsbgBVLleuYF0uggVlq1nnrxk1KSKB3k,218
2
2
  ergon/cli.py,sha256=CF8PvOjIguR18a657aEBEyhFXUoSVtSRVXv5PfnuWtA,6021
3
3
  ergon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  ergon/bootstrap/src/__project__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -78,7 +78,7 @@ ergon/task/helpers.py,sha256=Fzb2WsP--5f54VRo_oaPH3Hq9kIYoykfEDOhpd_QHXY,22879
78
78
  ergon/task/liveness.py,sha256=TsytIfmi2ua_x3KEPjTdx4WbqHk0WXTqeCSiQUBKPsE,1314
79
79
  ergon/task/manager.py,sha256=3Xr8RPRjtCEoupMeyYb19uK0TQhoVehFlBZd8ZFZ-YA,2972
80
80
  ergon/task/policies.py,sha256=mGb8iafrkVf438xiAUh0FpXqfgtNAM7RyoaF8jHNf80,7042
81
- ergon/task/runner.py,sha256=WmEAvkQiywPOIT03duHlFkAQhOf5XtnR2zvJlcEAp0g,16781
81
+ ergon/task/runner.py,sha256=OPCvhfTnbpSAs7Xj7qQZVS11_jm7Vzh8GCgySzT4L6k,19328
82
82
  ergon/task/utils.py,sha256=OmlP2AhLM32V8O9OMky6DdSvxwmMNn9x2LTguf0y-M0,2501
83
83
  ergon/task/mixins/__init__.py,sha256=6A8vNgdjaQYNJvymWNF-kBN2oTaQBYRLctS3Sq1w_-Y,320
84
84
  ergon/task/mixins/consumer.py,sha256=0S_ZU27cCn74LoDZdPGH-15wmpAtZI1HcS9Iqw2h3DE,46088
@@ -91,9 +91,9 @@ ergon/telemetry/metrics.py,sha256=0T_CuNw9P5AG1U9k1-bNp1g7McyiNLUIt_zKyLOtrnA,28
91
91
  ergon/telemetry/tracing.py,sha256=_ktiZWAgB1DU-uN91Mzr0iYNXxbCN21LnKiRo5SOJxY,4454
92
92
  ergon/utils/__init__.py,sha256=M0_VCjMN7jvSPlRZLZHeFKTHAl1zu3BnAhjrzJjDfiI,57
93
93
  ergon/utils/env.py,sha256=lji4xENvfHqg2GTGnzG95VKpxxM4f3DHIKKP61xQG08,621
94
- ergon_framework_python-0.1.5.dist-info/licenses/LICENSE,sha256=bWCIhHzaadz_ecLdCdtwyMafeQ17s2d7_FSSuSuEcDs,1079
95
- ergon_framework_python-0.1.5.dist-info/METADATA,sha256=H4wELHqsOmECRPZav1U5TOl_5zfu7o1louaspojMOhI,15677
96
- ergon_framework_python-0.1.5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
97
- ergon_framework_python-0.1.5.dist-info/entry_points.txt,sha256=reZ934txb5OrtcNBgTGqCsXa1biUDQWubD2a9fGZynE,42
98
- ergon_framework_python-0.1.5.dist-info/top_level.txt,sha256=KSPt6N7RzsjU7PRAalWQ9Cul7YTUxbo84yAJ-y2vLm8,6
99
- ergon_framework_python-0.1.5.dist-info/RECORD,,
94
+ ergon_framework_python-0.1.6.dist-info/licenses/LICENSE,sha256=bWCIhHzaadz_ecLdCdtwyMafeQ17s2d7_FSSuSuEcDs,1079
95
+ ergon_framework_python-0.1.6.dist-info/METADATA,sha256=0ylSq7-H-BKek2GmW3dj74svbykQyIlAXJuXf9esyao,15677
96
+ ergon_framework_python-0.1.6.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
97
+ ergon_framework_python-0.1.6.dist-info/entry_points.txt,sha256=reZ934txb5OrtcNBgTGqCsXa1biUDQWubD2a9fGZynE,42
98
+ ergon_framework_python-0.1.6.dist-info/top_level.txt,sha256=KSPt6N7RzsjU7PRAalWQ9Cul7YTUxbo84yAJ-y2vLm8,6
99
+ ergon_framework_python-0.1.6.dist-info/RECORD,,