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.
Files changed (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. 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