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,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