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,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from threading import Lock
|
|
5
|
+
from time import monotonic
|
|
6
|
+
from typing import Callable, Protocol, cast
|
|
7
|
+
|
|
8
|
+
from onec_runtime.errors import StaleRuntimeGeneration
|
|
9
|
+
from onec_runtime.recovery_journal import RecoveryJournal
|
|
10
|
+
from onec_runtime.supervisor_model import (
|
|
11
|
+
GenerationHandle,
|
|
12
|
+
GenerationLifecycle,
|
|
13
|
+
GenerationRecord,
|
|
14
|
+
GenerationRegistry,
|
|
15
|
+
TerminationCause,
|
|
16
|
+
)
|
|
17
|
+
from onec_runtime.supervisor_protocol import (
|
|
18
|
+
ControlMessage,
|
|
19
|
+
MessageKind,
|
|
20
|
+
MessageReceiver,
|
|
21
|
+
MessageSender,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
HEARTBEAT_WATCHDOG_S = 1.5
|
|
26
|
+
WORKER_TERMINATION_GRACE_S = 2.0
|
|
27
|
+
RUNTIME_TERMINATION_GRACE_S = 10.0
|
|
28
|
+
TERMINATION_BOUND_S = 25.0
|
|
29
|
+
JOURNAL_STREAM = "supervisor.jsonl"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GenerationProcesses(Protocol):
|
|
33
|
+
controller_pid: int | None
|
|
34
|
+
dbgs_pid: int | None
|
|
35
|
+
onec_pid: int | None
|
|
36
|
+
|
|
37
|
+
def controller_exitcode(self) -> int | None:
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
def terminate_controller(self, timeout_s: float) -> None:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
def terminate_onec(self, timeout_s: float) -> None:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def terminate_dbgs(self, timeout_s: float) -> None:
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
def all_stopped(self) -> bool:
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class SupervisorStatus:
|
|
55
|
+
generation: GenerationRecord | None
|
|
56
|
+
lifecycle: GenerationLifecycle | None
|
|
57
|
+
termination_cause: TerminationCause | None
|
|
58
|
+
last_progress_sequence: int | None
|
|
59
|
+
heartbeat_deadline: float | None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _RuntimeGenerationRegistry(GenerationRegistry):
|
|
63
|
+
"""Adds the Supervisor-only fail-closed transition from STARTING."""
|
|
64
|
+
|
|
65
|
+
def begin_startup_termination(
|
|
66
|
+
self,
|
|
67
|
+
handle: GenerationHandle,
|
|
68
|
+
cause: TerminationCause,
|
|
69
|
+
) -> GenerationRecord:
|
|
70
|
+
record = self._require_current(handle)
|
|
71
|
+
self._require_lifecycle(
|
|
72
|
+
record,
|
|
73
|
+
GenerationLifecycle.STARTING,
|
|
74
|
+
"begin startup termination",
|
|
75
|
+
)
|
|
76
|
+
self._current = replace(
|
|
77
|
+
record,
|
|
78
|
+
lifecycle=GenerationLifecycle.TERMINATING,
|
|
79
|
+
termination_cause=cause,
|
|
80
|
+
)
|
|
81
|
+
return self._current
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RuntimeSupervisor:
|
|
85
|
+
"""Owns one runtime generation and terminates it on loss of control."""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
process_factory: Callable[[int], GenerationProcesses],
|
|
90
|
+
*,
|
|
91
|
+
clock: Callable[[], float] = monotonic,
|
|
92
|
+
journal: RecoveryJournal | None = None,
|
|
93
|
+
heartbeat_timeout_s: float = HEARTBEAT_WATCHDOG_S,
|
|
94
|
+
) -> None:
|
|
95
|
+
if heartbeat_timeout_s <= 0:
|
|
96
|
+
raise ValueError("heartbeat_timeout_s must be positive")
|
|
97
|
+
self._process_factory = process_factory
|
|
98
|
+
self._clock = clock
|
|
99
|
+
self._journal = journal or RecoveryJournal()
|
|
100
|
+
self._heartbeat_timeout_s = heartbeat_timeout_s
|
|
101
|
+
self._registry = _RuntimeGenerationRegistry()
|
|
102
|
+
self._record: GenerationRecord | None = None
|
|
103
|
+
self._processes: GenerationProcesses | None = None
|
|
104
|
+
self._sender: MessageSender | None = None
|
|
105
|
+
self._receiver: MessageReceiver | None = None
|
|
106
|
+
self._last_progress_sequence: int | None = None
|
|
107
|
+
self._heartbeat_deadline: float | None = None
|
|
108
|
+
self._termination_detected_at: float | None = None
|
|
109
|
+
self._termination_deadline: float | None = None
|
|
110
|
+
self._termination_evidence_errors: list[BaseException] = []
|
|
111
|
+
self._cleanup_lock = Lock()
|
|
112
|
+
|
|
113
|
+
def begin_generation(self) -> GenerationHandle:
|
|
114
|
+
record = self._registry.begin_start()
|
|
115
|
+
self._record = record
|
|
116
|
+
self._processes = None
|
|
117
|
+
self._sender = None
|
|
118
|
+
self._receiver = None
|
|
119
|
+
self._processes = self._process_factory(record.handle.generation_id)
|
|
120
|
+
self._sender = MessageSender(record.handle.generation_id)
|
|
121
|
+
self._receiver = MessageReceiver(record.handle.generation_id)
|
|
122
|
+
self._last_progress_sequence = None
|
|
123
|
+
self._heartbeat_deadline = None
|
|
124
|
+
self._termination_detected_at = None
|
|
125
|
+
self._termination_deadline = None
|
|
126
|
+
self._termination_evidence_errors.clear()
|
|
127
|
+
return record.handle
|
|
128
|
+
|
|
129
|
+
def activate(self, handle: GenerationHandle) -> GenerationRecord:
|
|
130
|
+
processes = self._require_processes()
|
|
131
|
+
self._record = self._registry.activate(
|
|
132
|
+
handle,
|
|
133
|
+
controller_pid=cast(int, processes.controller_pid),
|
|
134
|
+
dbgs_pid=cast(int, processes.dbgs_pid),
|
|
135
|
+
onec_pid=cast(int, processes.onec_pid),
|
|
136
|
+
)
|
|
137
|
+
self._heartbeat_deadline = self._clock() + self._heartbeat_timeout_s
|
|
138
|
+
return self._record
|
|
139
|
+
|
|
140
|
+
def require_active(self, handle: GenerationHandle) -> GenerationRecord:
|
|
141
|
+
return self._registry.require_active(handle)
|
|
142
|
+
|
|
143
|
+
def on_worker_message(self, message: ControlMessage) -> None:
|
|
144
|
+
if (
|
|
145
|
+
message.kind is MessageKind.HEARTBEAT
|
|
146
|
+
and (
|
|
147
|
+
self._record is None
|
|
148
|
+
or message.generation_id != self._record.handle.generation_id
|
|
149
|
+
)
|
|
150
|
+
):
|
|
151
|
+
self.heartbeat(
|
|
152
|
+
GenerationHandle(message.generation_id),
|
|
153
|
+
progress_sequence=int(message.payload["progress_sequence"]),
|
|
154
|
+
busy_deadline=None,
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
receiver = self._receiver
|
|
159
|
+
if receiver is None:
|
|
160
|
+
raise RuntimeError("No runtime generation exists")
|
|
161
|
+
receiver.accept(message)
|
|
162
|
+
if message.kind is MessageKind.HEARTBEAT:
|
|
163
|
+
busy_deadline = message.payload.get("busy_deadline")
|
|
164
|
+
self.heartbeat(
|
|
165
|
+
GenerationHandle(message.generation_id),
|
|
166
|
+
progress_sequence=int(message.payload["progress_sequence"]),
|
|
167
|
+
busy_deadline=(
|
|
168
|
+
float(busy_deadline) if busy_deadline is not None else None
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
elif self._record is not None and (
|
|
172
|
+
self._record.lifecycle is GenerationLifecycle.ACTIVE
|
|
173
|
+
):
|
|
174
|
+
self._heartbeat_deadline = self._clock() + self._heartbeat_timeout_s
|
|
175
|
+
|
|
176
|
+
def heartbeat(
|
|
177
|
+
self,
|
|
178
|
+
handle: GenerationHandle,
|
|
179
|
+
progress_sequence: int,
|
|
180
|
+
busy_deadline: float | None,
|
|
181
|
+
) -> None:
|
|
182
|
+
try:
|
|
183
|
+
self._registry.require_active(handle)
|
|
184
|
+
except StaleRuntimeGeneration:
|
|
185
|
+
self._journal.record(
|
|
186
|
+
JOURNAL_STREAM,
|
|
187
|
+
"late_heartbeat_ignored",
|
|
188
|
+
generation_id=handle.generation_id,
|
|
189
|
+
progress_sequence=progress_sequence,
|
|
190
|
+
)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
now = self._clock()
|
|
194
|
+
self._last_progress_sequence = progress_sequence
|
|
195
|
+
self._heartbeat_deadline = (
|
|
196
|
+
busy_deadline
|
|
197
|
+
if busy_deadline is not None and busy_deadline > now
|
|
198
|
+
else now + self._heartbeat_timeout_s
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def poll(self) -> None:
|
|
202
|
+
cause = self.detect_termination_cause()
|
|
203
|
+
if cause is not None:
|
|
204
|
+
self.terminate(cause)
|
|
205
|
+
|
|
206
|
+
def detect_termination_cause(self) -> TerminationCause | None:
|
|
207
|
+
record = self._record
|
|
208
|
+
if record is None or record.lifecycle is not GenerationLifecycle.ACTIVE:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
processes = self._require_processes()
|
|
212
|
+
if processes.controller_exitcode() is not None:
|
|
213
|
+
return TerminationCause.CONTROLLER_EXIT
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
self._heartbeat_deadline is not None
|
|
217
|
+
and self._clock() >= self._heartbeat_deadline
|
|
218
|
+
):
|
|
219
|
+
return TerminationCause.CONTROLLER_HUNG
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def terminate(self, cause: TerminationCause) -> GenerationRecord:
|
|
223
|
+
record = self.begin_termination(cause)
|
|
224
|
+
if record.lifecycle is GenerationLifecycle.TERMINATED:
|
|
225
|
+
return record
|
|
226
|
+
return self.complete_termination()
|
|
227
|
+
|
|
228
|
+
def begin_termination(self, cause: TerminationCause) -> GenerationRecord:
|
|
229
|
+
record = self._record
|
|
230
|
+
if record is None:
|
|
231
|
+
raise RuntimeError("No runtime generation exists")
|
|
232
|
+
if record.lifecycle in {
|
|
233
|
+
GenerationLifecycle.TERMINATING,
|
|
234
|
+
GenerationLifecycle.TERMINATED,
|
|
235
|
+
}:
|
|
236
|
+
return record
|
|
237
|
+
|
|
238
|
+
detected_at = self._clock()
|
|
239
|
+
if record.lifecycle is GenerationLifecycle.STARTING:
|
|
240
|
+
record = self._registry.begin_startup_termination(record.handle, cause)
|
|
241
|
+
else:
|
|
242
|
+
record = self._registry.begin_termination(record.handle, cause)
|
|
243
|
+
self._record = record
|
|
244
|
+
self._termination_detected_at = detected_at
|
|
245
|
+
self._termination_deadline = detected_at + TERMINATION_BOUND_S
|
|
246
|
+
self._heartbeat_deadline = None
|
|
247
|
+
self._record_termination_evidence(
|
|
248
|
+
"generation_termination_planned",
|
|
249
|
+
generation_id=record.handle.generation_id,
|
|
250
|
+
cause=cause.value,
|
|
251
|
+
detected_at=detected_at,
|
|
252
|
+
)
|
|
253
|
+
return record
|
|
254
|
+
|
|
255
|
+
def complete_termination(self) -> GenerationRecord:
|
|
256
|
+
with self._cleanup_lock:
|
|
257
|
+
return self._complete_termination_owned()
|
|
258
|
+
|
|
259
|
+
def _complete_termination_owned(self) -> GenerationRecord:
|
|
260
|
+
record = self._record
|
|
261
|
+
if record is None:
|
|
262
|
+
raise RuntimeError("No runtime generation exists")
|
|
263
|
+
if record.lifecycle is GenerationLifecycle.TERMINATED:
|
|
264
|
+
return record
|
|
265
|
+
if record.lifecycle is not GenerationLifecycle.TERMINATING:
|
|
266
|
+
raise RuntimeError(
|
|
267
|
+
f"Cannot complete termination in {record.lifecycle.name} lifecycle"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
processes = self._processes
|
|
271
|
+
if processes is not None:
|
|
272
|
+
cleanup_errors: list[BaseException] = []
|
|
273
|
+
for terminate_process, timeout_cap_s in (
|
|
274
|
+
(processes.terminate_controller, WORKER_TERMINATION_GRACE_S),
|
|
275
|
+
(processes.terminate_onec, RUNTIME_TERMINATION_GRACE_S),
|
|
276
|
+
(processes.terminate_dbgs, RUNTIME_TERMINATION_GRACE_S),
|
|
277
|
+
):
|
|
278
|
+
timeout_s = min(
|
|
279
|
+
timeout_cap_s,
|
|
280
|
+
self._termination_time_remaining(),
|
|
281
|
+
)
|
|
282
|
+
try:
|
|
283
|
+
terminate_process(timeout_s)
|
|
284
|
+
except BaseException as error:
|
|
285
|
+
cleanup_errors.append(error)
|
|
286
|
+
if not processes.all_stopped():
|
|
287
|
+
incomplete = RuntimeError(
|
|
288
|
+
"Runtime generation processes did not stop"
|
|
289
|
+
)
|
|
290
|
+
if cleanup_errors:
|
|
291
|
+
raise incomplete from cleanup_errors[0]
|
|
292
|
+
raise incomplete
|
|
293
|
+
|
|
294
|
+
detected_at = self._termination_detected_at
|
|
295
|
+
if detected_at is None:
|
|
296
|
+
raise RuntimeError("Termination detection timestamp is unavailable")
|
|
297
|
+
cause = record.termination_cause
|
|
298
|
+
if cause is None:
|
|
299
|
+
raise RuntimeError("Termination cause is unavailable")
|
|
300
|
+
terminated_at = self._clock()
|
|
301
|
+
record = self._registry.finish_termination(record.handle)
|
|
302
|
+
self._record = record
|
|
303
|
+
duration_s = terminated_at - detected_at
|
|
304
|
+
self._record_termination_evidence(
|
|
305
|
+
"generation_terminated",
|
|
306
|
+
generation_id=record.handle.generation_id,
|
|
307
|
+
cause=cause.value,
|
|
308
|
+
detected_at=detected_at,
|
|
309
|
+
terminated_at=terminated_at,
|
|
310
|
+
duration_s=duration_s,
|
|
311
|
+
)
|
|
312
|
+
if duration_s > TERMINATION_BOUND_S:
|
|
313
|
+
self._record_termination_evidence(
|
|
314
|
+
"termination_bound_exceeded",
|
|
315
|
+
generation_id=record.handle.generation_id,
|
|
316
|
+
detected_at=detected_at,
|
|
317
|
+
terminated_at=terminated_at,
|
|
318
|
+
duration_s=duration_s,
|
|
319
|
+
)
|
|
320
|
+
self._raise_termination_evidence_errors()
|
|
321
|
+
return record
|
|
322
|
+
|
|
323
|
+
def status(self) -> SupervisorStatus:
|
|
324
|
+
record = self._record
|
|
325
|
+
return SupervisorStatus(
|
|
326
|
+
generation=record,
|
|
327
|
+
lifecycle=record.lifecycle if record is not None else None,
|
|
328
|
+
termination_cause=(
|
|
329
|
+
record.termination_cause if record is not None else None
|
|
330
|
+
),
|
|
331
|
+
last_progress_sequence=self._last_progress_sequence,
|
|
332
|
+
heartbeat_deadline=self._heartbeat_deadline,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def termination_time_remaining(self) -> float:
|
|
336
|
+
return self._termination_time_remaining()
|
|
337
|
+
|
|
338
|
+
def _require_processes(self) -> GenerationProcesses:
|
|
339
|
+
if self._processes is None:
|
|
340
|
+
raise RuntimeError("No runtime generation processes exist")
|
|
341
|
+
return self._processes
|
|
342
|
+
|
|
343
|
+
def _termination_time_remaining(self) -> float:
|
|
344
|
+
deadline = self._termination_deadline
|
|
345
|
+
if deadline is None:
|
|
346
|
+
raise RuntimeError("Termination deadline is unavailable")
|
|
347
|
+
return max(0.0, deadline - self._clock())
|
|
348
|
+
|
|
349
|
+
def _record_termination_evidence(self, event: str, **fields: object) -> None:
|
|
350
|
+
try:
|
|
351
|
+
self._journal.record(JOURNAL_STREAM, event, **fields)
|
|
352
|
+
self._journal.flush()
|
|
353
|
+
except BaseException as error:
|
|
354
|
+
self._termination_evidence_errors.append(error)
|
|
355
|
+
|
|
356
|
+
def _raise_termination_evidence_errors(self) -> None:
|
|
357
|
+
errors = self._termination_evidence_errors
|
|
358
|
+
if not errors:
|
|
359
|
+
return
|
|
360
|
+
self._termination_evidence_errors = []
|
|
361
|
+
if len(errors) == 1:
|
|
362
|
+
raise errors[0]
|
|
363
|
+
raise BaseExceptionGroup("Termination evidence failures", errors)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from onec_runtime.errors import StaleRuntimeGeneration
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GenerationLifecycle(str, Enum):
|
|
11
|
+
STARTING = "starting"
|
|
12
|
+
ACTIVE = "active"
|
|
13
|
+
TERMINATING = "terminating"
|
|
14
|
+
TERMINATED = "terminated"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TerminationCause(str, Enum):
|
|
18
|
+
CONTROLLER_EXIT = "controller_exit"
|
|
19
|
+
CONTROLLER_HUNG = "controller_hung"
|
|
20
|
+
LEASE_EXPIRED = "lease_expired"
|
|
21
|
+
STARTUP_FAILED = "startup_failed"
|
|
22
|
+
REQUESTED = "requested"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class GenerationHandle:
|
|
27
|
+
generation_id: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class LeaseHandle:
|
|
32
|
+
generation_id: int
|
|
33
|
+
owner_id: UUID
|
|
34
|
+
lease_epoch: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class SupervisedOperationHandle:
|
|
39
|
+
generation_id: int
|
|
40
|
+
operation_id: int
|
|
41
|
+
owner_id: UUID
|
|
42
|
+
lease_epoch: int
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class GenerationRecord:
|
|
47
|
+
handle: GenerationHandle
|
|
48
|
+
lifecycle: GenerationLifecycle
|
|
49
|
+
controller_pid: int | None = None
|
|
50
|
+
dbgs_pid: int | None = None
|
|
51
|
+
onec_pid: int | None = None
|
|
52
|
+
termination_cause: TerminationCause | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GenerationRegistry:
|
|
56
|
+
"""Owns the in-memory lifecycle of one supervised runtime generation."""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._last_generation_id = 0
|
|
60
|
+
self._current: GenerationRecord | None = None
|
|
61
|
+
|
|
62
|
+
def begin_start(self) -> GenerationRecord:
|
|
63
|
+
if self._current is not None and (
|
|
64
|
+
self._current.lifecycle is not GenerationLifecycle.TERMINATED
|
|
65
|
+
):
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
"Cannot start generation while "
|
|
68
|
+
f"{self._current.lifecycle.name} generation exists"
|
|
69
|
+
)
|
|
70
|
+
self._last_generation_id += 1
|
|
71
|
+
self._current = GenerationRecord(
|
|
72
|
+
handle=GenerationHandle(self._last_generation_id),
|
|
73
|
+
lifecycle=GenerationLifecycle.STARTING,
|
|
74
|
+
)
|
|
75
|
+
return self._current
|
|
76
|
+
|
|
77
|
+
def activate(
|
|
78
|
+
self,
|
|
79
|
+
handle: GenerationHandle,
|
|
80
|
+
*,
|
|
81
|
+
controller_pid: int,
|
|
82
|
+
dbgs_pid: int,
|
|
83
|
+
onec_pid: int,
|
|
84
|
+
) -> GenerationRecord:
|
|
85
|
+
record = self._require_current(handle)
|
|
86
|
+
self._require_lifecycle(record, GenerationLifecycle.STARTING, "activate")
|
|
87
|
+
self._current = replace(
|
|
88
|
+
record,
|
|
89
|
+
lifecycle=GenerationLifecycle.ACTIVE,
|
|
90
|
+
controller_pid=controller_pid,
|
|
91
|
+
dbgs_pid=dbgs_pid,
|
|
92
|
+
onec_pid=onec_pid,
|
|
93
|
+
)
|
|
94
|
+
return self._current
|
|
95
|
+
|
|
96
|
+
def begin_termination(
|
|
97
|
+
self,
|
|
98
|
+
handle: GenerationHandle,
|
|
99
|
+
cause: TerminationCause,
|
|
100
|
+
) -> GenerationRecord:
|
|
101
|
+
record = self._require_current(handle)
|
|
102
|
+
self._require_lifecycle(
|
|
103
|
+
record,
|
|
104
|
+
GenerationLifecycle.ACTIVE,
|
|
105
|
+
"begin termination",
|
|
106
|
+
)
|
|
107
|
+
self._current = replace(
|
|
108
|
+
record,
|
|
109
|
+
lifecycle=GenerationLifecycle.TERMINATING,
|
|
110
|
+
termination_cause=cause,
|
|
111
|
+
)
|
|
112
|
+
return self._current
|
|
113
|
+
|
|
114
|
+
def finish_termination(self, handle: GenerationHandle) -> GenerationRecord:
|
|
115
|
+
record = self._require_current(handle)
|
|
116
|
+
self._require_lifecycle(
|
|
117
|
+
record,
|
|
118
|
+
GenerationLifecycle.TERMINATING,
|
|
119
|
+
"finish termination",
|
|
120
|
+
)
|
|
121
|
+
self._current = replace(record, lifecycle=GenerationLifecycle.TERMINATED)
|
|
122
|
+
return self._current
|
|
123
|
+
|
|
124
|
+
def require_active(self, handle: GenerationHandle) -> GenerationRecord:
|
|
125
|
+
record = self._require_current(handle)
|
|
126
|
+
if record.lifecycle is not GenerationLifecycle.ACTIVE:
|
|
127
|
+
raise StaleRuntimeGeneration(
|
|
128
|
+
f"Generation {handle.generation_id} is not active"
|
|
129
|
+
)
|
|
130
|
+
return record
|
|
131
|
+
|
|
132
|
+
def _require_current(self, handle: GenerationHandle) -> GenerationRecord:
|
|
133
|
+
if self._current is None or self._current.handle != handle:
|
|
134
|
+
raise StaleRuntimeGeneration(
|
|
135
|
+
f"Generation {handle.generation_id} is no longer current"
|
|
136
|
+
)
|
|
137
|
+
return self._current
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _require_lifecycle(
|
|
141
|
+
record: GenerationRecord,
|
|
142
|
+
expected: GenerationLifecycle,
|
|
143
|
+
action: str,
|
|
144
|
+
) -> None:
|
|
145
|
+
if record.lifecycle is not expected:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
f"Cannot {action} generation in {record.lifecycle.name} lifecycle"
|
|
148
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from onec_runtime.errors import InvalidMessageSequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PROTOCOL_VERSION = 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageKind(str, Enum):
|
|
13
|
+
DEBUG_READY = "debug_ready"
|
|
14
|
+
DEBUGGEE_STARTED = "debuggee_started"
|
|
15
|
+
READY = "ready"
|
|
16
|
+
HEARTBEAT = "heartbeat"
|
|
17
|
+
GRANT_LEASE = "grant_lease"
|
|
18
|
+
RENEW_LEASE = "renew_lease"
|
|
19
|
+
LEASE_GRANTED = "lease_granted"
|
|
20
|
+
LEASE_CONFLICT = "lease_conflict"
|
|
21
|
+
LEASE_EXPIRED = "lease_expired"
|
|
22
|
+
EXECUTE_PHASE = "execute_phase"
|
|
23
|
+
PHASE_REACHED = "phase_reached"
|
|
24
|
+
OPERATION_RESULT = "operation_result"
|
|
25
|
+
STATUS = "status"
|
|
26
|
+
STATUS_RESULT = "status_result"
|
|
27
|
+
SHUTDOWN = "shutdown"
|
|
28
|
+
FATAL = "fatal"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class ControlMessage:
|
|
33
|
+
protocol_version: int
|
|
34
|
+
generation_id: int
|
|
35
|
+
message_sequence: int
|
|
36
|
+
kind: MessageKind
|
|
37
|
+
payload: dict[str, object]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MessageSender:
|
|
41
|
+
def __init__(self, generation_id: int) -> None:
|
|
42
|
+
self._generation_id = generation_id
|
|
43
|
+
self._message_sequence = 0
|
|
44
|
+
|
|
45
|
+
def create(self, kind: MessageKind, **payload: object) -> ControlMessage:
|
|
46
|
+
self._message_sequence += 1
|
|
47
|
+
return ControlMessage(
|
|
48
|
+
protocol_version=PROTOCOL_VERSION,
|
|
49
|
+
generation_id=self._generation_id,
|
|
50
|
+
message_sequence=self._message_sequence,
|
|
51
|
+
kind=kind,
|
|
52
|
+
payload=payload,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MessageReceiver:
|
|
57
|
+
def __init__(self, generation_id: int) -> None:
|
|
58
|
+
self._generation_id = generation_id
|
|
59
|
+
self._expected_sequence = 1
|
|
60
|
+
|
|
61
|
+
def accept(self, message: ControlMessage) -> None:
|
|
62
|
+
if message.protocol_version != PROTOCOL_VERSION:
|
|
63
|
+
raise InvalidMessageSequence(
|
|
64
|
+
"Unsupported protocol version "
|
|
65
|
+
f"{message.protocol_version}; expected {PROTOCOL_VERSION}"
|
|
66
|
+
)
|
|
67
|
+
if message.generation_id != self._generation_id:
|
|
68
|
+
raise InvalidMessageSequence(
|
|
69
|
+
f"Message generation {message.generation_id} does not match "
|
|
70
|
+
f"receiver generation {self._generation_id}"
|
|
71
|
+
)
|
|
72
|
+
if message.message_sequence != self._expected_sequence:
|
|
73
|
+
raise InvalidMessageSequence(
|
|
74
|
+
f"Message sequence {message.message_sequence} is not the "
|
|
75
|
+
f"expected sequence {self._expected_sequence}"
|
|
76
|
+
)
|
|
77
|
+
self._expected_sequence += 1
|