quapp-common 0.0.15.dev2__tar.gz → 0.0.15.dev4__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.
- {quapp_common-0.0.15.dev2/quapp_common.egg-info → quapp_common-0.0.15.dev4}/PKG-INFO +1 -1
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/pyproject.toml +1 -1
- quapp_common-0.0.15.dev4/quapp_common/async_tasks/async_invocation_task.py +209 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_manager.py +13 -4
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/thread_config.py +17 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4/quapp_common.egg-info}/PKG-INFO +1 -1
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/SOURCES.txt +1 -0
- quapp_common-0.0.15.dev4/tests/test_compute_process.py +919 -0
- quapp_common-0.0.15.dev2/quapp_common/async_tasks/async_invocation_task.py +0 -116
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/LICENSE +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/README.md +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/post_processing_task.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/invocation.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_fetcher.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_fetching.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/bridge.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/dispatcher.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/log_context.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/logging_config.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/error_report.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/error_category.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/runtime_phase.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/device/custom_device.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/device/device.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/invocation.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/error_classifier.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/invocation_failure.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/response_utils.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/setup.cfg +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_callback_payload.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_dispatcher_runner_error.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_error_classifier.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_error_log_dedupe.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_error_report.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_invocation_failure.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_log_secret_leak.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_logging_config.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_no_sdk_dependency.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_phase_coverage.py +0 -0
- {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_result_serializer.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quapp-common"
|
|
7
|
-
version = "0.0.15.
|
|
7
|
+
version = "0.0.15.dev4"
|
|
8
8
|
description = "Quapp common library supporting Quapp Platform for Quantum Computing"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# async_invocation_task.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
import asyncio
|
|
5
|
+
import ctypes
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from starlette.responses import JSONResponse
|
|
13
|
+
|
|
14
|
+
from ..async_tasks.async_task import AsyncTask
|
|
15
|
+
from ..component.backend.job_heartbeat import TERMINAL_STATUSES, ensure_started
|
|
16
|
+
from ..component.backend.job_manager import JobManager
|
|
17
|
+
from ..config.log_context import bind_job
|
|
18
|
+
from ..config.logging_config import logger
|
|
19
|
+
from ..factory.handler_factory import HandlerFactory
|
|
20
|
+
from ..model.invocation import Event, Error, Success
|
|
21
|
+
from ..util.invocation_failure import report_invocation_failure
|
|
22
|
+
|
|
23
|
+
PR_SET_PDEATHSIG = 1
|
|
24
|
+
ORPHAN_EXIT_CODE = 3
|
|
25
|
+
PARENT_POLL_SEC = 5.0
|
|
26
|
+
COMPUTE_PROCESS_PREFIX = 'job-compute'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fork_context():
|
|
30
|
+
return multiprocessing.get_context('fork')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_parent_death_signal() -> bool:
|
|
34
|
+
try:
|
|
35
|
+
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
|
36
|
+
return libc.prctl(PR_SET_PDEATHSIG, int(signal.SIGKILL), 0, 0, 0) == 0
|
|
37
|
+
except Exception:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def describe_exit_code(exit_code: int) -> str:
|
|
42
|
+
if exit_code == -int(signal.SIGKILL):
|
|
43
|
+
return ('compute process was killed with SIGKILL, which usually means '
|
|
44
|
+
'the container ran out of memory')
|
|
45
|
+
if exit_code is not None and exit_code < 0:
|
|
46
|
+
try:
|
|
47
|
+
name = signal.Signals(-exit_code).name
|
|
48
|
+
except ValueError:
|
|
49
|
+
name = f'signal {-exit_code}'
|
|
50
|
+
return f'compute process was terminated by {name}'
|
|
51
|
+
if exit_code == ORPHAN_EXIT_CODE:
|
|
52
|
+
return 'compute process exited because its parent process was gone'
|
|
53
|
+
return f'compute process exited with code {exit_code}'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _watch_parent(parent_pid: int) -> None:
|
|
57
|
+
while True:
|
|
58
|
+
if os.getppid() != parent_pid:
|
|
59
|
+
os._exit(ORPHAN_EXIT_CODE)
|
|
60
|
+
time.sleep(PARENT_POLL_SEC)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_compute(parent_pid: int, target) -> None:
|
|
64
|
+
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
65
|
+
|
|
66
|
+
if not set_parent_death_signal():
|
|
67
|
+
logger.warning(
|
|
68
|
+
'Could not arm PR_SET_PDEATHSIG for the compute process; the '
|
|
69
|
+
'only remaining guard is a polling thread, which cannot run '
|
|
70
|
+
'while native code holds the GIL, so an orphaned compute '
|
|
71
|
+
'process may keep a GPU or a scheduler slot busy')
|
|
72
|
+
|
|
73
|
+
if os.getppid() != parent_pid:
|
|
74
|
+
os._exit(ORPHAN_EXIT_CODE)
|
|
75
|
+
|
|
76
|
+
watcher = threading.Thread(target=_watch_parent, args=(parent_pid,),
|
|
77
|
+
name='compute-parent-watch', daemon=True)
|
|
78
|
+
watcher.start()
|
|
79
|
+
|
|
80
|
+
target()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AsyncInvocationTask(AsyncTask):
|
|
84
|
+
"""
|
|
85
|
+
Handles asynchronous invocation tasks, designed to process events with
|
|
86
|
+
specific handlers and generate immediate responses.
|
|
87
|
+
|
|
88
|
+
The class is aimed at enabling tasks to be processed asynchronously, involving
|
|
89
|
+
a handler factory and functions for event processing and post-processing. It
|
|
90
|
+
executes the handler in a dedicated child process to ensure quick response to
|
|
91
|
+
clients, allowing them to poll for job status updates. Its primary purpose is
|
|
92
|
+
to facilitate efficient asynchronous processing of jobs without blocking the
|
|
93
|
+
main application flow.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
handler_factory: Factory instance that produces job handlers.
|
|
97
|
+
event: Event to be processed by the created handler.
|
|
98
|
+
processing_fn: Function to be executed during the job processing stage.
|
|
99
|
+
post_processing_fn: Function to be executed post-job processing.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, handler_factory: HandlerFactory, event: Event,
|
|
103
|
+
processing_fn, post_processing_fn):
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.handler_factory = handler_factory
|
|
106
|
+
self.event = event
|
|
107
|
+
self.processing_fn = processing_fn
|
|
108
|
+
self.post_processing_fn = post_processing_fn
|
|
109
|
+
|
|
110
|
+
def do(self) -> JSONResponse:
|
|
111
|
+
"""
|
|
112
|
+
Handles executing a job in a child process and provides an immediate response
|
|
113
|
+
with the job's ID and status. Logging is configured dynamically based on the
|
|
114
|
+
provided job ID.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
JSONResponse: A response object with the status and job ID if the job submission succeeds,
|
|
118
|
+
or an error message if there is an issue.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
Exception: If any error occurs during job processing, it is logged, and an appropriate error
|
|
122
|
+
response is returned.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
job_id = self.event.json().get('jobId')
|
|
126
|
+
if job_id is None:
|
|
127
|
+
raise Exception("Job ID is missing from event")
|
|
128
|
+
|
|
129
|
+
# This used to call logger.add(sink=sys.stderr, ...) for EVERY
|
|
130
|
+
# request: after N jobs there were N+1 sinks and every line printed
|
|
131
|
+
# N+1 times. The sinks are configured once in logging_config; all
|
|
132
|
+
# that is needed here is binding the context so every later line
|
|
133
|
+
# carries job_id/trace_id on its own.
|
|
134
|
+
job_log = bind_job(job_id)
|
|
135
|
+
|
|
136
|
+
def _run_handle():
|
|
137
|
+
return self.handler_factory.create_handler(self.event,
|
|
138
|
+
self.processing_fn,
|
|
139
|
+
self.post_processing_fn).handle()
|
|
140
|
+
|
|
141
|
+
JobManager.ensure_registry()
|
|
142
|
+
ensure_started()
|
|
143
|
+
|
|
144
|
+
loop = asyncio.get_running_loop()
|
|
145
|
+
process = fork_context().Process(
|
|
146
|
+
target=run_compute, args=(os.getpid(), _run_handle),
|
|
147
|
+
name=f'{COMPUTE_PROCESS_PREFIX}-{job_id}', daemon=False)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
process.start()
|
|
151
|
+
except Exception as start_exception:
|
|
152
|
+
job_log.bind(event='compute_process_start_failed').exception(
|
|
153
|
+
f'Could not start the compute process for job '
|
|
154
|
+
f'{job_id}: {start_exception}')
|
|
155
|
+
JobManager.set_failed(job_id)
|
|
156
|
+
report_invocation_failure(self.event, start_exception,
|
|
157
|
+
phase=None,
|
|
158
|
+
context={'jobId': job_id})
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
self._watch_compute_process(loop, process, job_id, job_log)
|
|
162
|
+
|
|
163
|
+
# Respond immediately so clients can poll GET /jobs/{job_id}
|
|
164
|
+
return JSONResponse(status_code=200, content=Success(
|
|
165
|
+
JobManager.get_job(job_id)).serialize())
|
|
166
|
+
except Exception as exception:
|
|
167
|
+
logger.exception(f"An error occurred: {exception}")
|
|
168
|
+
return JSONResponse(status_code=400,
|
|
169
|
+
content=Error(exception).serialize())
|
|
170
|
+
|
|
171
|
+
def _watch_compute_process(self, loop, process, job_id, job_log) -> None:
|
|
172
|
+
sentinel = process.sentinel
|
|
173
|
+
|
|
174
|
+
def _on_process_exit():
|
|
175
|
+
loop.remove_reader(sentinel)
|
|
176
|
+
process.join()
|
|
177
|
+
exit_code = process.exitcode
|
|
178
|
+
if exit_code == 0:
|
|
179
|
+
return
|
|
180
|
+
self._report_compute_failure(loop, job_id, job_log, exit_code)
|
|
181
|
+
|
|
182
|
+
loop.add_reader(sentinel, _on_process_exit)
|
|
183
|
+
|
|
184
|
+
def _report_compute_failure(self, loop, job_id, job_log,
|
|
185
|
+
exit_code) -> None:
|
|
186
|
+
reason = describe_exit_code(exit_code)
|
|
187
|
+
|
|
188
|
+
if JobManager.get_status(job_id) in TERMINAL_STATUSES:
|
|
189
|
+
job_log.bind(event='compute_process_exit_ignored').info(
|
|
190
|
+
f'Job {job_id} already reached a terminal state, ignoring '
|
|
191
|
+
f'that the {reason}')
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
job_log.bind(event='compute_process_failed').error(
|
|
195
|
+
f'Job {job_id} lost its compute process: the {reason}')
|
|
196
|
+
JobManager.set_failed(job_id)
|
|
197
|
+
loop.run_in_executor(None, self._send_failure_report, job_id, job_log,
|
|
198
|
+
exit_code, reason)
|
|
199
|
+
|
|
200
|
+
def _send_failure_report(self, job_id, job_log, exit_code, reason) -> None:
|
|
201
|
+
try:
|
|
202
|
+
report_invocation_failure(self.event, RuntimeError(reason),
|
|
203
|
+
phase=None,
|
|
204
|
+
context={'jobId' : job_id,
|
|
205
|
+
'exitCode': exit_code})
|
|
206
|
+
except Exception as exception:
|
|
207
|
+
job_log.bind(event='callback_failed').exception(
|
|
208
|
+
f'Failed to report the lost compute process of job '
|
|
209
|
+
f'{job_id}: {exception}')
|
{quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_manager.py
RENAMED
|
@@ -8,7 +8,7 @@ import atexit
|
|
|
8
8
|
import threading
|
|
9
9
|
import time
|
|
10
10
|
from datetime import timezone, datetime
|
|
11
|
-
from multiprocessing import Manager, current_process
|
|
11
|
+
from multiprocessing import Manager, current_process, parent_process
|
|
12
12
|
from typing import Any, Callable, Dict, Iterable, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from ...config.logging_config import logger
|
|
@@ -61,6 +61,14 @@ class JobManager:
|
|
|
61
61
|
# Ensure a clean shutdown in this process
|
|
62
62
|
atexit.register(cls._shutdown)
|
|
63
63
|
|
|
64
|
+
@classmethod
|
|
65
|
+
def ensure_registry(cls) -> None:
|
|
66
|
+
cls._ensure_initialized()
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def is_root_process(cls) -> bool:
|
|
70
|
+
return parent_process() is None
|
|
71
|
+
|
|
64
72
|
@classmethod
|
|
65
73
|
def _shutdown(cls) -> None:
|
|
66
74
|
"""
|
|
@@ -107,8 +115,9 @@ class JobManager:
|
|
|
107
115
|
cls._jobs[job_id] = data # proxy is process-safe
|
|
108
116
|
cls._publish(job_id, {"event": "added", **data})
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
if cls.is_root_process():
|
|
119
|
+
from .job_heartbeat import ensure_started
|
|
120
|
+
ensure_started()
|
|
112
121
|
|
|
113
122
|
@classmethod
|
|
114
123
|
def update_status(cls, job_id: str, status: str,
|
|
@@ -138,7 +147,7 @@ class JobManager:
|
|
|
138
147
|
cls._publish(job_id, {"event": "updated", **new_data})
|
|
139
148
|
|
|
140
149
|
from .job_heartbeat import TERMINAL_STATUSES, ensure_started
|
|
141
|
-
if status not in TERMINAL_STATUSES:
|
|
150
|
+
if status not in TERMINAL_STATUSES and cls.is_root_process():
|
|
142
151
|
ensure_started()
|
|
143
152
|
return None
|
|
144
153
|
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
QApp Platform Project thread_config.py Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
3
3
|
"""
|
|
4
4
|
import contextvars
|
|
5
|
+
import os
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
5
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
9
|
|
|
7
10
|
|
|
@@ -27,3 +30,17 @@ circuit_exporting_pool = ContextAwareThreadPoolExecutor(
|
|
|
27
30
|
|
|
28
31
|
circuit_running_pool = ContextAwareThreadPoolExecutor(
|
|
29
32
|
max_workers=20, thread_name_prefix='circuit-running-pool-')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def reset_pools_after_fork() -> None:
|
|
36
|
+
for pool in (circuit_exporting_pool, circuit_running_pool):
|
|
37
|
+
pool._threads = set()
|
|
38
|
+
pool._idle_semaphore = threading.Semaphore(0)
|
|
39
|
+
pool._work_queue = queue.SimpleQueue()
|
|
40
|
+
pool._shutdown = False
|
|
41
|
+
pool._shutdown_lock = threading.Lock()
|
|
42
|
+
pool._broken = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if hasattr(os, 'register_at_fork'):
|
|
46
|
+
os.register_at_fork(after_in_child=reset_pools_after_fork)
|
|
@@ -91,6 +91,7 @@ quapp_common/util/json_parser_utils.py
|
|
|
91
91
|
quapp_common/util/response_utils.py
|
|
92
92
|
tests/test_callback_payload.py
|
|
93
93
|
tests/test_circuit_adapter.py
|
|
94
|
+
tests/test_compute_process.py
|
|
94
95
|
tests/test_dispatcher_runner_error.py
|
|
95
96
|
tests/test_error_classifier.py
|
|
96
97
|
tests/test_error_log_dedupe.py
|