onec-interactive-runtime-core 0.1.19__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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from threading import Lock, Thread
|
|
8
|
+
from time import monotonic, sleep
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from onec_runtime.controller_worker import BarrierMode
|
|
13
|
+
from onec_runtime.errors import (
|
|
14
|
+
CommandTimeout,
|
|
15
|
+
ControllerUnavailable,
|
|
16
|
+
InvalidMessageSequence,
|
|
17
|
+
LeaseConflict,
|
|
18
|
+
LeaseExpired,
|
|
19
|
+
StaleLeaseEpoch,
|
|
20
|
+
StaleRuntimeGeneration,
|
|
21
|
+
)
|
|
22
|
+
from onec_runtime.fault_injection import FaultPoint
|
|
23
|
+
from onec_runtime.lease import validate_lease_ttl
|
|
24
|
+
from onec_runtime.recovery_journal import RecoveryJournal
|
|
25
|
+
from onec_runtime.supervisor import (
|
|
26
|
+
HEARTBEAT_WATCHDOG_S,
|
|
27
|
+
JOURNAL_STREAM,
|
|
28
|
+
WORKER_TERMINATION_GRACE_S,
|
|
29
|
+
GenerationProcesses,
|
|
30
|
+
RuntimeSupervisor,
|
|
31
|
+
)
|
|
32
|
+
from onec_runtime.supervisor_model import (
|
|
33
|
+
GenerationHandle,
|
|
34
|
+
GenerationLifecycle,
|
|
35
|
+
GenerationRecord,
|
|
36
|
+
LeaseHandle,
|
|
37
|
+
SupervisedOperationHandle,
|
|
38
|
+
TerminationCause,
|
|
39
|
+
)
|
|
40
|
+
from onec_runtime.supervisor_protocol import (
|
|
41
|
+
ControlMessage,
|
|
42
|
+
MessageKind,
|
|
43
|
+
MessageSender,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
DEBUG_READY_TIMEOUT_S = 10.0
|
|
48
|
+
READY_TIMEOUT_S = 90.0
|
|
49
|
+
LEASE_TTL_S = 2.0
|
|
50
|
+
SUPERVISOR_POLL_INTERVAL_S = 0.05
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StagedGenerationProcesses(GenerationProcesses, Protocol):
|
|
54
|
+
def start_debug_server(self) -> int:
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
def start_controller(self, debug_port: int, run_dir: Path) -> None:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def start_debuggee(self, debug_port: int) -> None:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def send(self, message: ControlMessage) -> None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
def receive(self, timeout_s: float) -> ControlMessage:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_TYPED_WORKER_ERRORS: dict[str, type[Exception]] = {
|
|
71
|
+
error_type.__name__: error_type
|
|
72
|
+
for error_type in (
|
|
73
|
+
ControllerUnavailable,
|
|
74
|
+
LeaseConflict,
|
|
75
|
+
LeaseExpired,
|
|
76
|
+
StaleLeaseEpoch,
|
|
77
|
+
StaleRuntimeGeneration,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SupervisedRuntime:
|
|
83
|
+
"""High-level staged control plane for one supervised runtime generation."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
processes_factory: Callable[[int], StagedGenerationProcesses],
|
|
88
|
+
*,
|
|
89
|
+
journal: RecoveryJournal | None = None,
|
|
90
|
+
heartbeat_timeout_s: float = HEARTBEAT_WATCHDOG_S,
|
|
91
|
+
run_dir: Path | None = None,
|
|
92
|
+
lease_ttl_s: float = LEASE_TTL_S,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._journal = journal or RecoveryJournal()
|
|
95
|
+
self._run_dir = run_dir or Path.cwd()
|
|
96
|
+
self._lease_ttl_s = validate_lease_ttl(lease_ttl_s)
|
|
97
|
+
self._processes_factory = processes_factory
|
|
98
|
+
self._processes: StagedGenerationProcesses | None = None
|
|
99
|
+
self._sender: MessageSender | None = None
|
|
100
|
+
self._pending: deque[ControlMessage] = deque()
|
|
101
|
+
self._next_operation_id = 0
|
|
102
|
+
self._termination_thread: Thread | None = None
|
|
103
|
+
self._termination_error: BaseException | None = None
|
|
104
|
+
self._termination_thread_lock = Lock()
|
|
105
|
+
self._supervisor = RuntimeSupervisor(
|
|
106
|
+
self._create_processes,
|
|
107
|
+
journal=self._journal,
|
|
108
|
+
heartbeat_timeout_s=heartbeat_timeout_s,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def start(self) -> GenerationHandle:
|
|
112
|
+
try:
|
|
113
|
+
handle = self._supervisor.begin_generation()
|
|
114
|
+
self._sender = MessageSender(handle.generation_id)
|
|
115
|
+
self._pending.clear()
|
|
116
|
+
self._next_operation_id = 0
|
|
117
|
+
processes = self._require_processes()
|
|
118
|
+
debug_port = processes.start_debug_server()
|
|
119
|
+
processes.start_controller(debug_port, self._run_dir)
|
|
120
|
+
self._receive_startup(MessageKind.DEBUG_READY, DEBUG_READY_TIMEOUT_S)
|
|
121
|
+
processes.start_debuggee(debug_port)
|
|
122
|
+
self._send(MessageKind.DEBUGGEE_STARTED)
|
|
123
|
+
self._receive_startup(MessageKind.READY, READY_TIMEOUT_S)
|
|
124
|
+
self._supervisor.activate(handle)
|
|
125
|
+
return handle
|
|
126
|
+
except BaseException:
|
|
127
|
+
try:
|
|
128
|
+
self._terminate_failed_startup()
|
|
129
|
+
except BaseException:
|
|
130
|
+
pass
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
def grant_lease(
|
|
134
|
+
self,
|
|
135
|
+
generation: GenerationHandle,
|
|
136
|
+
owner_id: UUID,
|
|
137
|
+
*,
|
|
138
|
+
ttl_s: float | None = None,
|
|
139
|
+
) -> LeaseHandle:
|
|
140
|
+
self._supervisor.require_active(generation)
|
|
141
|
+
validated_ttl_s = validate_lease_ttl(
|
|
142
|
+
self._lease_ttl_s if ttl_s is None else ttl_s
|
|
143
|
+
)
|
|
144
|
+
self._send(
|
|
145
|
+
MessageKind.GRANT_LEASE,
|
|
146
|
+
owner_id=str(owner_id),
|
|
147
|
+
ttl_s=validated_ttl_s,
|
|
148
|
+
)
|
|
149
|
+
response = self._wait_for_response(MessageKind.LEASE_GRANTED)
|
|
150
|
+
handle = LeaseHandle(
|
|
151
|
+
generation_id=generation.generation_id,
|
|
152
|
+
owner_id=owner_id,
|
|
153
|
+
lease_epoch=self._positive_int(
|
|
154
|
+
response.payload["lease_epoch"],
|
|
155
|
+
"lease_epoch",
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
self._record_owner_evidence("lease_granted", handle)
|
|
159
|
+
return handle
|
|
160
|
+
|
|
161
|
+
def renew_lease(
|
|
162
|
+
self,
|
|
163
|
+
lease: LeaseHandle,
|
|
164
|
+
*,
|
|
165
|
+
ttl_s: float | None = None,
|
|
166
|
+
) -> LeaseHandle:
|
|
167
|
+
self._supervisor.require_active(GenerationHandle(lease.generation_id))
|
|
168
|
+
validated_ttl_s = validate_lease_ttl(
|
|
169
|
+
self._lease_ttl_s if ttl_s is None else ttl_s
|
|
170
|
+
)
|
|
171
|
+
self._send(
|
|
172
|
+
MessageKind.RENEW_LEASE,
|
|
173
|
+
owner_id=str(lease.owner_id),
|
|
174
|
+
lease_epoch=lease.lease_epoch,
|
|
175
|
+
ttl_s=validated_ttl_s,
|
|
176
|
+
)
|
|
177
|
+
response = self._wait_for_response(MessageKind.LEASE_GRANTED)
|
|
178
|
+
renewed = LeaseHandle(
|
|
179
|
+
generation_id=lease.generation_id,
|
|
180
|
+
owner_id=lease.owner_id,
|
|
181
|
+
lease_epoch=self._positive_int(
|
|
182
|
+
response.payload["lease_epoch"],
|
|
183
|
+
"lease_epoch",
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
self._record_owner_evidence("lease_renewed", renewed)
|
|
187
|
+
return renewed
|
|
188
|
+
|
|
189
|
+
def execute_phase(
|
|
190
|
+
self,
|
|
191
|
+
lease: LeaseHandle,
|
|
192
|
+
point: FaultPoint,
|
|
193
|
+
*,
|
|
194
|
+
barrier_mode: BarrierMode = BarrierMode.CRASH,
|
|
195
|
+
wait_for_result: bool = True,
|
|
196
|
+
) -> SupervisedOperationHandle | ControlMessage:
|
|
197
|
+
self._supervisor.require_active(GenerationHandle(lease.generation_id))
|
|
198
|
+
self._next_operation_id += 1
|
|
199
|
+
operation = SupervisedOperationHandle(
|
|
200
|
+
generation_id=lease.generation_id,
|
|
201
|
+
operation_id=self._next_operation_id,
|
|
202
|
+
owner_id=lease.owner_id,
|
|
203
|
+
lease_epoch=lease.lease_epoch,
|
|
204
|
+
)
|
|
205
|
+
self._send(
|
|
206
|
+
MessageKind.EXECUTE_PHASE,
|
|
207
|
+
owner_id=str(lease.owner_id),
|
|
208
|
+
lease_epoch=lease.lease_epoch,
|
|
209
|
+
operation_id=operation.operation_id,
|
|
210
|
+
point=point.value,
|
|
211
|
+
barrier_mode=barrier_mode.value,
|
|
212
|
+
)
|
|
213
|
+
if not wait_for_result:
|
|
214
|
+
return operation
|
|
215
|
+
return self._wait_for_response(
|
|
216
|
+
MessageKind.OPERATION_RESULT,
|
|
217
|
+
operation_id=operation.operation_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def wait_for_event(
|
|
221
|
+
self,
|
|
222
|
+
kind: MessageKind,
|
|
223
|
+
*,
|
|
224
|
+
timeout_s: float,
|
|
225
|
+
) -> ControlMessage:
|
|
226
|
+
return self._wait_for_response(kind, timeout_s=timeout_s)
|
|
227
|
+
|
|
228
|
+
def status(self, observer_id: str) -> ControlMessage:
|
|
229
|
+
record = self._supervisor.status().generation
|
|
230
|
+
if record is None:
|
|
231
|
+
raise StaleRuntimeGeneration("No runtime generation is current")
|
|
232
|
+
self._supervisor.require_active(record.handle)
|
|
233
|
+
self._send(MessageKind.STATUS, observer_id=observer_id)
|
|
234
|
+
return self._wait_for_response(MessageKind.STATUS_RESULT)
|
|
235
|
+
|
|
236
|
+
def kill_controller(self) -> None:
|
|
237
|
+
record = self._supervisor.status().generation
|
|
238
|
+
if record is None:
|
|
239
|
+
raise StaleRuntimeGeneration("No runtime generation is current")
|
|
240
|
+
self._supervisor.require_active(record.handle)
|
|
241
|
+
self._require_processes().terminate_controller(WORKER_TERMINATION_GRACE_S)
|
|
242
|
+
|
|
243
|
+
def wait_terminated(self, timeout_s: float) -> GenerationRecord:
|
|
244
|
+
deadline = monotonic() + timeout_s
|
|
245
|
+
while True:
|
|
246
|
+
status = self._supervisor.status()
|
|
247
|
+
if (
|
|
248
|
+
status.generation is not None
|
|
249
|
+
and status.lifecycle is GenerationLifecycle.TERMINATED
|
|
250
|
+
and self._all_stopped()
|
|
251
|
+
and not self._termination_owner_is_alive()
|
|
252
|
+
):
|
|
253
|
+
self._raise_terminal_termination_error()
|
|
254
|
+
return status.generation
|
|
255
|
+
|
|
256
|
+
remaining = deadline - monotonic()
|
|
257
|
+
if remaining <= 0:
|
|
258
|
+
raise CommandTimeout(
|
|
259
|
+
f"Runtime generation did not terminate within {timeout_s} seconds"
|
|
260
|
+
)
|
|
261
|
+
self._poll_once(min(SUPERVISOR_POLL_INTERVAL_S, remaining))
|
|
262
|
+
|
|
263
|
+
def close(self) -> None:
|
|
264
|
+
status = self._supervisor.status()
|
|
265
|
+
if status.generation is None:
|
|
266
|
+
return
|
|
267
|
+
if status.lifecycle in {
|
|
268
|
+
GenerationLifecycle.STARTING,
|
|
269
|
+
GenerationLifecycle.ACTIVE,
|
|
270
|
+
}:
|
|
271
|
+
self._begin_async_termination(TerminationCause.REQUESTED)
|
|
272
|
+
self._wait_for_cleanup_owner()
|
|
273
|
+
|
|
274
|
+
def _create_processes(self, generation_id: int) -> StagedGenerationProcesses:
|
|
275
|
+
self._processes = None
|
|
276
|
+
processes = self._processes_factory(generation_id)
|
|
277
|
+
self._processes = processes
|
|
278
|
+
return processes
|
|
279
|
+
|
|
280
|
+
def _receive_startup(self, expected: MessageKind, timeout_s: float) -> None:
|
|
281
|
+
message = self._require_processes().receive(timeout_s)
|
|
282
|
+
self._accept_worker_message(message)
|
|
283
|
+
self._raise_worker_error(message)
|
|
284
|
+
if message.kind is not expected:
|
|
285
|
+
raise InvalidMessageSequence(
|
|
286
|
+
f"Expected {expected.value}, received {message.kind.value}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def _send(self, kind: MessageKind, **payload: object) -> None:
|
|
290
|
+
sender = self._sender
|
|
291
|
+
if sender is None:
|
|
292
|
+
raise ControllerUnavailable("No Controller sender exists")
|
|
293
|
+
self._require_processes().send(sender.create(kind, **payload))
|
|
294
|
+
|
|
295
|
+
def _wait_for_response(
|
|
296
|
+
self,
|
|
297
|
+
expected: MessageKind,
|
|
298
|
+
*,
|
|
299
|
+
timeout_s: float = READY_TIMEOUT_S,
|
|
300
|
+
operation_id: int | None = None,
|
|
301
|
+
) -> ControlMessage:
|
|
302
|
+
deadline = monotonic() + timeout_s
|
|
303
|
+
while True:
|
|
304
|
+
pending = self._take_pending(expected, operation_id)
|
|
305
|
+
if pending is not None:
|
|
306
|
+
return pending
|
|
307
|
+
self._raise_if_generation_unavailable()
|
|
308
|
+
|
|
309
|
+
remaining = deadline - monotonic()
|
|
310
|
+
if remaining <= 0:
|
|
311
|
+
raise CommandTimeout(
|
|
312
|
+
f"Controller did not publish {expected.value} within "
|
|
313
|
+
f"{timeout_s} seconds"
|
|
314
|
+
)
|
|
315
|
+
poll_timeout = min(SUPERVISOR_POLL_INTERVAL_S, remaining)
|
|
316
|
+
poll_started = monotonic()
|
|
317
|
+
message = self._receive_once(poll_timeout)
|
|
318
|
+
if message is not None:
|
|
319
|
+
self._raise_worker_error(message)
|
|
320
|
+
if self._matches(message, expected, operation_id):
|
|
321
|
+
return message
|
|
322
|
+
if message.kind is not MessageKind.HEARTBEAT:
|
|
323
|
+
self._pending.append(message)
|
|
324
|
+
self._finish_poll_interval(poll_started, poll_timeout)
|
|
325
|
+
self._poll_supervisor()
|
|
326
|
+
self._raise_if_generation_unavailable()
|
|
327
|
+
|
|
328
|
+
def _poll_once(self, timeout_s: float) -> None:
|
|
329
|
+
poll_started = monotonic()
|
|
330
|
+
message = self._receive_once(timeout_s)
|
|
331
|
+
if message is not None and message.kind is not MessageKind.HEARTBEAT:
|
|
332
|
+
self._pending.append(message)
|
|
333
|
+
self._finish_poll_interval(poll_started, timeout_s)
|
|
334
|
+
self._poll_supervisor()
|
|
335
|
+
|
|
336
|
+
def _poll_supervisor(self) -> None:
|
|
337
|
+
cause = self._supervisor.detect_termination_cause()
|
|
338
|
+
if cause is not None:
|
|
339
|
+
self._begin_async_termination(cause)
|
|
340
|
+
return
|
|
341
|
+
if self._supervisor.status().lifecycle is GenerationLifecycle.TERMINATING:
|
|
342
|
+
if self._supervisor.termination_time_remaining() > 0:
|
|
343
|
+
self._ensure_termination_thread()
|
|
344
|
+
|
|
345
|
+
def _receive_once(self, timeout_s: float) -> ControlMessage | None:
|
|
346
|
+
try:
|
|
347
|
+
message = self._require_processes().receive(timeout_s)
|
|
348
|
+
except (CommandTimeout, EOFError, OSError):
|
|
349
|
+
return None
|
|
350
|
+
self._accept_worker_message(message)
|
|
351
|
+
return message
|
|
352
|
+
|
|
353
|
+
def _accept_worker_message(self, message: ControlMessage) -> None:
|
|
354
|
+
self._supervisor.on_worker_message(message)
|
|
355
|
+
if message.kind is MessageKind.LEASE_EXPIRED:
|
|
356
|
+
self._begin_async_termination(TerminationCause.LEASE_EXPIRED)
|
|
357
|
+
|
|
358
|
+
def _begin_async_termination(self, cause: TerminationCause) -> None:
|
|
359
|
+
self._supervisor.begin_termination(cause)
|
|
360
|
+
self._ensure_termination_thread()
|
|
361
|
+
|
|
362
|
+
def _ensure_termination_thread(self) -> None:
|
|
363
|
+
with self._termination_thread_lock:
|
|
364
|
+
thread = self._termination_thread
|
|
365
|
+
if thread is not None and thread.is_alive():
|
|
366
|
+
return
|
|
367
|
+
if self._supervisor.termination_time_remaining() <= 0:
|
|
368
|
+
return
|
|
369
|
+
self._termination_error = None
|
|
370
|
+
thread = Thread(
|
|
371
|
+
target=self._complete_termination,
|
|
372
|
+
name="onec-runtime-termination",
|
|
373
|
+
daemon=True,
|
|
374
|
+
)
|
|
375
|
+
self._termination_thread = thread
|
|
376
|
+
thread.start()
|
|
377
|
+
|
|
378
|
+
def _complete_termination(self) -> None:
|
|
379
|
+
try:
|
|
380
|
+
self._supervisor.complete_termination()
|
|
381
|
+
except BaseException as error:
|
|
382
|
+
self._termination_error = error
|
|
383
|
+
|
|
384
|
+
def _raise_if_generation_unavailable(self) -> None:
|
|
385
|
+
status = self._supervisor.status()
|
|
386
|
+
if status.lifecycle not in {
|
|
387
|
+
GenerationLifecycle.TERMINATING,
|
|
388
|
+
GenerationLifecycle.TERMINATED,
|
|
389
|
+
}:
|
|
390
|
+
return
|
|
391
|
+
cause = status.termination_cause
|
|
392
|
+
cause_name = cause.value if cause is not None else "unknown"
|
|
393
|
+
raise ControllerUnavailable(
|
|
394
|
+
f"Runtime generation is unavailable after {cause_name}"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def _finish_poll_interval(started_at: float, interval_s: float) -> None:
|
|
399
|
+
deadline = started_at + interval_s
|
|
400
|
+
while True:
|
|
401
|
+
remaining = deadline - monotonic()
|
|
402
|
+
if remaining <= 0:
|
|
403
|
+
return
|
|
404
|
+
sleep(remaining)
|
|
405
|
+
|
|
406
|
+
def _take_pending(
|
|
407
|
+
self,
|
|
408
|
+
expected: MessageKind,
|
|
409
|
+
operation_id: int | None,
|
|
410
|
+
) -> ControlMessage | None:
|
|
411
|
+
for message in tuple(self._pending):
|
|
412
|
+
self._raise_worker_error(message)
|
|
413
|
+
if self._matches(message, expected, operation_id):
|
|
414
|
+
self._pending.remove(message)
|
|
415
|
+
return message
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def _matches(
|
|
420
|
+
message: ControlMessage,
|
|
421
|
+
expected: MessageKind,
|
|
422
|
+
operation_id: int | None,
|
|
423
|
+
) -> bool:
|
|
424
|
+
return message.kind is expected and (
|
|
425
|
+
operation_id is None
|
|
426
|
+
or message.payload.get("operation_id") == operation_id
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def _raise_worker_error(message: ControlMessage) -> None:
|
|
431
|
+
if message.kind is MessageKind.LEASE_EXPIRED:
|
|
432
|
+
raise LeaseExpired("The generation lease has expired")
|
|
433
|
+
if message.kind is MessageKind.FATAL:
|
|
434
|
+
raise ControllerUnavailable(
|
|
435
|
+
f"Controller failed with {message.payload.get('error_type', 'unknown')}"
|
|
436
|
+
)
|
|
437
|
+
if message.kind is not MessageKind.LEASE_CONFLICT:
|
|
438
|
+
return
|
|
439
|
+
error_name = str(message.payload.get("error_type", "LeaseConflict"))
|
|
440
|
+
error_type = _TYPED_WORKER_ERRORS.get(error_name, LeaseConflict)
|
|
441
|
+
raise error_type(f"Controller rejected request with {error_name}")
|
|
442
|
+
|
|
443
|
+
def _record_owner_evidence(self, event: str, lease: LeaseHandle) -> None:
|
|
444
|
+
self._journal.record(
|
|
445
|
+
JOURNAL_STREAM,
|
|
446
|
+
event,
|
|
447
|
+
generation_id=lease.generation_id,
|
|
448
|
+
owner_sha256=sha256(lease.owner_id.bytes).hexdigest(),
|
|
449
|
+
lease_epoch=lease.lease_epoch,
|
|
450
|
+
)
|
|
451
|
+
self._journal.flush()
|
|
452
|
+
|
|
453
|
+
def _terminate_failed_startup(self) -> None:
|
|
454
|
+
status = self._supervisor.status()
|
|
455
|
+
if status.generation is None:
|
|
456
|
+
return
|
|
457
|
+
if status.lifecycle is GenerationLifecycle.STARTING:
|
|
458
|
+
self._supervisor.terminate(TerminationCause.STARTUP_FAILED)
|
|
459
|
+
|
|
460
|
+
def _require_processes(self) -> StagedGenerationProcesses:
|
|
461
|
+
if self._processes is None:
|
|
462
|
+
raise RuntimeError("No runtime generation processes exist")
|
|
463
|
+
return self._processes
|
|
464
|
+
|
|
465
|
+
def _all_stopped(self) -> bool:
|
|
466
|
+
processes = self._processes
|
|
467
|
+
return processes is None or processes.all_stopped()
|
|
468
|
+
|
|
469
|
+
def _wait_for_cleanup_owner(self) -> None:
|
|
470
|
+
while True:
|
|
471
|
+
status = self._supervisor.status()
|
|
472
|
+
if status.lifecycle is GenerationLifecycle.TERMINATED:
|
|
473
|
+
thread = self._termination_thread
|
|
474
|
+
if thread is not None and thread.is_alive():
|
|
475
|
+
remaining = self._supervisor.termination_time_remaining()
|
|
476
|
+
thread.join(remaining)
|
|
477
|
+
if thread.is_alive():
|
|
478
|
+
raise CommandTimeout(
|
|
479
|
+
"Runtime generation cleanup exceeded its deadline"
|
|
480
|
+
)
|
|
481
|
+
self._raise_terminal_termination_error()
|
|
482
|
+
return
|
|
483
|
+
if status.lifecycle is not GenerationLifecycle.TERMINATING:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
remaining = self._supervisor.termination_time_remaining()
|
|
487
|
+
if remaining <= 0:
|
|
488
|
+
raise CommandTimeout("Runtime generation cleanup exceeded its deadline")
|
|
489
|
+
self._ensure_termination_thread()
|
|
490
|
+
thread = self._termination_thread
|
|
491
|
+
if thread is None:
|
|
492
|
+
raise RuntimeError("Runtime generation cleanup owner is unavailable")
|
|
493
|
+
thread.join(remaining)
|
|
494
|
+
if thread.is_alive():
|
|
495
|
+
raise CommandTimeout("Runtime generation cleanup exceeded its deadline")
|
|
496
|
+
|
|
497
|
+
def _termination_owner_is_alive(self) -> bool:
|
|
498
|
+
thread = self._termination_thread
|
|
499
|
+
return thread is not None and thread.is_alive()
|
|
500
|
+
|
|
501
|
+
def _raise_terminal_termination_error(self) -> None:
|
|
502
|
+
error = self._termination_error
|
|
503
|
+
if error is None:
|
|
504
|
+
return
|
|
505
|
+
self._termination_error = None
|
|
506
|
+
raise error
|
|
507
|
+
|
|
508
|
+
@staticmethod
|
|
509
|
+
def _positive_int(value: object, field_name: str) -> int:
|
|
510
|
+
if type(value) is not int or value <= 0:
|
|
511
|
+
raise InvalidMessageSequence(f"{field_name} must be a positive integer")
|
|
512
|
+
return value
|