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.
- {quapp_common-0.0.15.dev1/quapp_common.egg-info → quapp_common-0.0.15.dev2}/PKG-INFO +1 -1
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/pyproject.toml +1 -1
- quapp_common-0.0.15.dev2/quapp_common/component/backend/job_heartbeat.py +164 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_manager.py +10 -2
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2/quapp_common.egg-info}/PKG-INFO +1 -1
- quapp_common-0.0.15.dev2/tests/test_job_heartbeat.py +618 -0
- quapp_common-0.0.15.dev1/quapp_common/component/backend/job_heartbeat.py +0 -83
- quapp_common-0.0.15.dev1/tests/test_job_heartbeat.py +0 -115
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/LICENSE +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/README.md +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/async_invocation_task.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/async_tasks/post_processing_task.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/invocation.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_fetcher.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_fetching.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/bridge.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/dispatcher.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/log_context.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/logging_config.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/config/thread_config.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/error_report.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/error_category.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/runtime_phase.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/device/custom_device.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/device/device.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/invocation.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/error_classifier.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/invocation_failure.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/util/response_utils.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/SOURCES.txt +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/setup.cfg +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_callback_payload.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_dispatcher_runner_error.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_error_classifier.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_error_log_dedupe.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_error_report.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_invocation_failure.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_log_secret_leak.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_logging_config.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_no_sdk_dependency.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/tests/test_phase_coverage.py +0 -0
- {quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/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.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)
|
{quapp_common-0.0.15.dev1 → quapp_common-0.0.15.dev2}/quapp_common/component/backend/job_manager.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|