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.
Files changed (110) hide show
  1. {quapp_common-0.0.15.dev2/quapp_common.egg-info → quapp_common-0.0.15.dev4}/PKG-INFO +1 -1
  2. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/pyproject.toml +1 -1
  3. quapp_common-0.0.15.dev4/quapp_common/async_tasks/async_invocation_task.py +209 -0
  4. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_manager.py +13 -4
  5. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/thread_config.py +17 -0
  6. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4/quapp_common.egg-info}/PKG-INFO +1 -1
  7. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/SOURCES.txt +1 -0
  8. quapp_common-0.0.15.dev4/tests/test_compute_process.py +919 -0
  9. quapp_common-0.0.15.dev2/quapp_common/async_tasks/async_invocation_task.py +0 -116
  10. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/LICENSE +0 -0
  11. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/README.md +0 -0
  12. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/__init__.py +0 -0
  13. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/__init__.py +0 -0
  14. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/async_task.py +0 -0
  15. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  16. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/async_tasks/post_processing_task.py +0 -0
  17. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/__init__.py +0 -0
  18. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/__init__.py +0 -0
  19. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/invocation.py +0 -0
  20. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_fetcher.py +0 -0
  21. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_fetching.py +0 -0
  22. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/backend/job_heartbeat.py +0 -0
  23. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/bridge.py +0 -0
  24. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/callback/__init__.py +0 -0
  25. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/callback/update_job_metadata.py +0 -0
  26. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/circuit_adapter.py +0 -0
  27. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/device/__init__.py +0 -0
  28. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/device/device_selection.py +0 -0
  29. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/dispatcher.py +0 -0
  30. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/component/result_serializer.py +0 -0
  31. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/__init__.py +0 -0
  32. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/log_context.py +0 -0
  33. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/config/logging_config.py +0 -0
  34. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/__init__.py +0 -0
  35. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/__init__.py +0 -0
  36. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  37. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  38. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  39. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/backend/__init__.py +0 -0
  40. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/backend/backend_information.py +0 -0
  41. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/callback/__init__.py +0 -0
  42. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/callback/callback_url.py +0 -0
  43. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/device/__init__.py +0 -0
  44. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/device/circuit_running_option.py +0 -0
  45. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/promise/__init__.py +0 -0
  46. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/promise/post_processing_promise.py +0 -0
  47. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/promise/promise.py +0 -0
  48. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/__init__.py +0 -0
  49. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/invocation_request.py +0 -0
  50. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/job_fetching_request.py +0 -0
  51. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/request/request.py +0 -0
  52. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/__init__.py +0 -0
  53. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/authentication.py +0 -0
  54. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/custom_header.py +0 -0
  55. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/error_report.py +0 -0
  56. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/data/response/job_response.py +0 -0
  57. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/__init__.py +0 -0
  58. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/base_enum.py +0 -0
  59. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/error_category.py +0 -0
  60. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/http_header.py +0 -0
  61. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/invocation_step.py +0 -0
  62. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/language.py +0 -0
  63. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/media_type.py +0 -0
  64. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/processing_unit.py +0 -0
  65. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/provider_tag.py +0 -0
  66. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/runtime_phase.py +0 -0
  67. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/sdk.py +0 -0
  68. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/status/__init__.py +0 -0
  69. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/status/job_status.py +0 -0
  70. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/status/status_code.py +0 -0
  71. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/enum/token_type.py +0 -0
  72. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/__init__.py +0 -0
  73. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/device_factory.py +0 -0
  74. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/handler_factory.py +0 -0
  75. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/factory/provider_factory.py +0 -0
  76. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/handler/__init__.py +0 -0
  77. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/handler/handler.py +0 -0
  78. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/__init__.py +0 -0
  79. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/device/__init__.py +0 -0
  80. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/device/custom_device.py +0 -0
  81. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/device/device.py +0 -0
  82. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/invocation.py +0 -0
  83. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/provider/__init__.py +0 -0
  84. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/model/provider/provider.py +0 -0
  85. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/__init__.py +0 -0
  86. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/error_classifier.py +0 -0
  87. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/file_utils.py +0 -0
  88. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/http_utils.py +0 -0
  89. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/invocation_failure.py +0 -0
  90. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/json_parser_utils.py +0 -0
  91. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common/util/response_utils.py +0 -0
  92. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/dependency_links.txt +0 -0
  93. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/requires.txt +0 -0
  94. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/quapp_common.egg-info/top_level.txt +0 -0
  95. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/setup.cfg +0 -0
  96. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_callback_payload.py +0 -0
  97. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_circuit_adapter.py +0 -0
  98. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_dispatcher_runner_error.py +0 -0
  99. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_error_classifier.py +0 -0
  100. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_error_log_dedupe.py +0 -0
  101. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_error_report.py +0 -0
  102. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_invocation_failure.py +0 -0
  103. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_job_heartbeat.py +0 -0
  104. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_json_parser_utils.py +0 -0
  105. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_language_dispatcher.py +0 -0
  106. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_log_secret_leak.py +0 -0
  107. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_logging_config.py +0 -0
  108. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_no_sdk_dependency.py +0 -0
  109. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_phase_coverage.py +0 -0
  110. {quapp_common-0.0.15.dev2 → quapp_common-0.0.15.dev4}/tests/test_result_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.15.dev2
3
+ Version: 0.0.15.dev4
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.15.dev2"
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}')
@@ -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
- from .job_heartbeat import ensure_started
111
- ensure_started()
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.15.dev2
3
+ Version: 0.0.15.dev4
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -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