quapp-common 0.0.15.dev1__tar.gz → 0.0.15.dev2__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.dev1/quapp_common.egg-info → quapp_common-0.0.15.dev2}/PKG-INFO +1 -1
  2. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/pyproject.toml +1 -1
  3. quapp_common-0.0.15.dev2/quapp_common/component/backend/job_heartbeat.py +164 -0
  4. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_manager.py +10 -2
  5. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2/quapp_common.egg-info}/PKG-INFO +1 -1
  6. quapp_common-0.0.15.dev2/tests/test_job_heartbeat.py +618 -0
  7. quapp_common-0.0.15.dev1/quapp_common/component/backend/job_heartbeat.py +0 -83
  8. quapp_common-0.0.15.dev1/tests/test_job_heartbeat.py +0 -115
  9. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/LICENSE +0 -0
  10. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/README.md +0 -0
  11. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/__init__.py +0 -0
  12. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/__init__.py +0 -0
  13. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/async_invocation_task.py +0 -0
  14. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/async_task.py +0 -0
  15. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  16. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/post_processing_task.py +0 -0
  17. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/__init__.py +0 -0
  18. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/__init__.py +0 -0
  19. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/invocation.py +0 -0
  20. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_fetcher.py +0 -0
  21. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_fetching.py +0 -0
  22. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/bridge.py +0 -0
  23. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/callback/__init__.py +0 -0
  24. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/callback/update_job_metadata.py +0 -0
  25. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/circuit_adapter.py +0 -0
  26. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/device/__init__.py +0 -0
  27. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/device/device_selection.py +0 -0
  28. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/dispatcher.py +0 -0
  29. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/result_serializer.py +0 -0
  30. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/__init__.py +0 -0
  31. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/log_context.py +0 -0
  32. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/logging_config.py +0 -0
  33. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/thread_config.py +0 -0
  34. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/__init__.py +0 -0
  35. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/__init__.py +0 -0
  36. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  37. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  38. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  39. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/backend/__init__.py +0 -0
  40. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/backend/backend_information.py +0 -0
  41. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/callback/__init__.py +0 -0
  42. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/callback/callback_url.py +0 -0
  43. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/device/__init__.py +0 -0
  44. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/device/circuit_running_option.py +0 -0
  45. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/promise/__init__.py +0 -0
  46. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/promise/post_processing_promise.py +0 -0
  47. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/promise/promise.py +0 -0
  48. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/__init__.py +0 -0
  49. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/invocation_request.py +0 -0
  50. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/job_fetching_request.py +0 -0
  51. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/request.py +0 -0
  52. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/__init__.py +0 -0
  53. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/authentication.py +0 -0
  54. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/custom_header.py +0 -0
  55. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/error_report.py +0 -0
  56. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/job_response.py +0 -0
  57. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/__init__.py +0 -0
  58. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/base_enum.py +0 -0
  59. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/error_category.py +0 -0
  60. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/http_header.py +0 -0
  61. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/invocation_step.py +0 -0
  62. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/language.py +0 -0
  63. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/media_type.py +0 -0
  64. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/processing_unit.py +0 -0
  65. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/provider_tag.py +0 -0
  66. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/runtime_phase.py +0 -0
  67. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/sdk.py +0 -0
  68. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/status/__init__.py +0 -0
  69. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/status/job_status.py +0 -0
  70. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/status/status_code.py +0 -0
  71. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/token_type.py +0 -0
  72. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/__init__.py +0 -0
  73. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/device_factory.py +0 -0
  74. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/handler_factory.py +0 -0
  75. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/provider_factory.py +0 -0
  76. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/handler/__init__.py +0 -0
  77. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/handler/handler.py +0 -0
  78. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/__init__.py +0 -0
  79. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/device/__init__.py +0 -0
  80. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/device/custom_device.py +0 -0
  81. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/device/device.py +0 -0
  82. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/invocation.py +0 -0
  83. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/provider/__init__.py +0 -0
  84. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/provider/provider.py +0 -0
  85. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/__init__.py +0 -0
  86. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/error_classifier.py +0 -0
  87. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/file_utils.py +0 -0
  88. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/http_utils.py +0 -0
  89. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/invocation_failure.py +0 -0
  90. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/json_parser_utils.py +0 -0
  91. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/response_utils.py +0 -0
  92. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/SOURCES.txt +0 -0
  93. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/dependency_links.txt +0 -0
  94. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/requires.txt +0 -0
  95. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/top_level.txt +0 -0
  96. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/setup.cfg +0 -0
  97. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_callback_payload.py +0 -0
  98. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_circuit_adapter.py +0 -0
  99. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_dispatcher_runner_error.py +0 -0
  100. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_error_classifier.py +0 -0
  101. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_error_log_dedupe.py +0 -0
  102. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_error_report.py +0 -0
  103. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_invocation_failure.py +0 -0
  104. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_json_parser_utils.py +0 -0
  105. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_language_dispatcher.py +0 -0
  106. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_log_secret_leak.py +0 -0
  107. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_logging_config.py +0 -0
  108. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_no_sdk_dependency.py +0 -0
  109. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_phase_coverage.py +0 -0
  110. {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/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.dev1
3
+ Version: 0.0.15.dev2
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.dev1"
7
+ version = "0.0.15.dev2"
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,164 @@
1
+ # Quapp Platform Project
2
+ # job_heartbeat.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ import math
6
+ import multiprocessing
7
+ import os
8
+ import sys
9
+ import threading
10
+ import time
11
+
12
+ import requests
13
+
14
+ from ...config.logging_config import logger
15
+
16
+ REQUEST_TIMEOUT_SEC = 5.0
17
+
18
+ DEFAULT_INTERVAL_SEC = 30.0
19
+ MAX_INTERVAL_SEC = DEFAULT_INTERVAL_SEC
20
+ ORPHAN_POLL_SEC = 5.0
21
+ STOP_GRACE_SEC = 1.0
22
+ TERMINAL_STATUSES = ('DONE', 'FAILED')
23
+
24
+ _context = None
25
+ _condition = threading.Condition()
26
+ _process = None
27
+ _stop_event = None
28
+ _starting = False
29
+
30
+
31
+ def heartbeat_interval_sec() -> float:
32
+ raw = os.getenv('HEARTBEAT_INTERVAL_SEC', '')
33
+ try:
34
+ value = float(raw)
35
+ except ValueError:
36
+ return DEFAULT_INTERVAL_SEC
37
+ if math.isfinite(value) and 0 < value <= MAX_INTERVAL_SEC:
38
+ return value
39
+ return DEFAULT_INTERVAL_SEC
40
+
41
+
42
+ def _log_safely(level: str, message: str) -> None:
43
+ try:
44
+ getattr(logger, level)(message)
45
+ except Exception as exception:
46
+ try:
47
+ print(f'[job-heartbeat] {level}: {message} ({exception})',
48
+ file=sys.stderr, flush=True)
49
+ except Exception:
50
+ pass
51
+
52
+
53
+ def beat_once() -> int:
54
+ from .job_manager import JobManager, META, STATUS, SCHEDULER_CALLBACK_URL
55
+
56
+ sent = 0
57
+ for job_id, data in JobManager.list_jobs().items():
58
+ if data.get(STATUS) in TERMINAL_STATUSES:
59
+ continue
60
+ url = (data.get(META) or {}).get(SCHEDULER_CALLBACK_URL)
61
+ if not url:
62
+ continue
63
+ try:
64
+ response = requests.patch(
65
+ url, json={'status': 'RUNNING', 'heartbeat': True},
66
+ timeout=REQUEST_TIMEOUT_SEC)
67
+ sent += 1
68
+ _log_safely('debug',
69
+ f'Heartbeat for job {job_id} returned {response.status_code}')
70
+ except Exception as exception:
71
+ _log_safely('debug',
72
+ f'Heartbeat for job {job_id} failed: {exception}')
73
+ return sent
74
+
75
+
76
+ def _wait_for_next_beat(parent_pid: int, stop_event, interval: float) -> bool:
77
+ deadline = time.monotonic() + interval
78
+ while True:
79
+ remaining = deadline - time.monotonic()
80
+ if remaining <= 0:
81
+ return True
82
+ if stop_event.wait(min(remaining, ORPHAN_POLL_SEC)):
83
+ return False
84
+ if os.getppid() != parent_pid:
85
+ return False
86
+
87
+
88
+ def _heartbeat_loop(parent_pid: int, stop_event) -> None:
89
+ while os.getppid() == parent_pid:
90
+ try:
91
+ beat_once()
92
+ except Exception as exception:
93
+ _log_safely('exception', f'Heartbeat loop error: {exception}')
94
+ if not _wait_for_next_beat(parent_pid, stop_event,
95
+ heartbeat_interval_sec()):
96
+ return
97
+
98
+
99
+ def _is_running_or_starting(process) -> bool:
100
+ if process is None:
101
+ return False
102
+ return process.pid is None or process.is_alive()
103
+
104
+
105
+ def _shutdown_process(process, stop_event) -> None:
106
+ if stop_event is not None:
107
+ stop_event.set()
108
+
109
+ if process is None or process.pid is None:
110
+ return
111
+
112
+ process.join(timeout=STOP_GRACE_SEC)
113
+ if process.is_alive():
114
+ process.terminate()
115
+ process.join(timeout=STOP_GRACE_SEC)
116
+ if process.is_alive():
117
+ process.kill()
118
+ process.join(timeout=STOP_GRACE_SEC)
119
+
120
+
121
+ def ensure_started() -> None:
122
+ global _context, _process, _stop_event, _starting
123
+
124
+ with _condition:
125
+ while _starting:
126
+ _condition.wait()
127
+ if _is_running_or_starting(_process):
128
+ return
129
+ if _context is None:
130
+ _context = multiprocessing.get_context('fork')
131
+ stop_event = _context.Event()
132
+ process = _context.Process(
133
+ target=_heartbeat_loop, args=(os.getpid(), stop_event),
134
+ name='job-heartbeat', daemon=True)
135
+ _stop_event = stop_event
136
+ _process = process
137
+ _starting = True
138
+
139
+ try:
140
+ process.start()
141
+ finally:
142
+ with _condition:
143
+ _starting = False
144
+ if process.pid is None and _process is process:
145
+ _process = None
146
+ _stop_event = None
147
+ _condition.notify_all()
148
+
149
+ logger.info(
150
+ f'Job heartbeat started with interval {heartbeat_interval_sec()}s')
151
+
152
+
153
+ def stop() -> None:
154
+ global _process, _stop_event
155
+
156
+ with _condition:
157
+ while _starting:
158
+ _condition.wait()
159
+ process = _process
160
+ stop_event = _stop_event
161
+ _process = None
162
+ _stop_event = None
163
+
164
+ _shutdown_process(process, stop_event)
@@ -64,10 +64,14 @@ class JobManager:
64
64
  @classmethod
65
65
  def _shutdown(cls) -> None:
66
66
  """
67
- Stop any listener threads in this process and shutdown Manager.
67
+ Stop the heartbeat process, then any listener threads in this process,
68
+ then shutdown Manager.
68
69
  """
69
70
  from .job_heartbeat import stop
70
- stop()
71
+ try:
72
+ stop()
73
+ except Exception as exception:
74
+ logger.exception(f'Error stopping job heartbeat: {exception}')
71
75
 
72
76
  # stop listeners
73
77
  for key, (t, stop_ev) in list(cls._listener_threads.items()):
@@ -132,6 +136,10 @@ class JobManager:
132
136
  UPDATED_AT: cls._get_current_utc_time()}
133
137
  cls._jobs[job_id] = new_data
134
138
  cls._publish(job_id, {"event": "updated", **new_data})
139
+
140
+ from .job_heartbeat import TERMINAL_STATUSES, ensure_started
141
+ if status not in TERMINAL_STATUSES:
142
+ ensure_started()
135
143
  return None
136
144
 
137
145
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.15.dev1
3
+ Version: 0.0.15.dev2
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)