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,3772 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from hmac import compare_digest, digest
|
|
8
|
+
import json
|
|
9
|
+
from re import fullmatch
|
|
10
|
+
from secrets import token_bytes, token_urlsafe
|
|
11
|
+
from threading import RLock
|
|
12
|
+
from typing import Callable, Literal, Protocol
|
|
13
|
+
from uuid import UUID, uuid4
|
|
14
|
+
from weakref import WeakKeyDictionary
|
|
15
|
+
|
|
16
|
+
from onec_runtime.bsl.diagnostics import (
|
|
17
|
+
VisibleSourceContext,
|
|
18
|
+
WorkerDiagnosticArtifact,
|
|
19
|
+
parse_platform_diagnostic,
|
|
20
|
+
remap_worker_stage_diagnostic,
|
|
21
|
+
)
|
|
22
|
+
from onec_runtime.bsl.lexer import KEYWORDS
|
|
23
|
+
from onec_runtime.bsl.module_universe import (
|
|
24
|
+
DependencyUse,
|
|
25
|
+
LoweredWorkerModule,
|
|
26
|
+
ModuleDependencyBinding,
|
|
27
|
+
WorkerSemanticAdmission,
|
|
28
|
+
worker_semantic_admission,
|
|
29
|
+
)
|
|
30
|
+
from onec_runtime.bsl.semantic_lowering import WorkerExport
|
|
31
|
+
from onec_runtime.bsl.source_maps import (
|
|
32
|
+
LineIndex,
|
|
33
|
+
MappedSource,
|
|
34
|
+
SourceUnitKind,
|
|
35
|
+
SourceUnitRef,
|
|
36
|
+
)
|
|
37
|
+
from onec_runtime.errors import (
|
|
38
|
+
BslExecutionError,
|
|
39
|
+
ProtocolError,
|
|
40
|
+
WorkerPromotionOutcomeUnknown,
|
|
41
|
+
)
|
|
42
|
+
from onec_runtime.performance_profile import PhaseRecorder
|
|
43
|
+
from onec_runtime.rdbg.models import ModuleLocation
|
|
44
|
+
from onec_runtime.server_worker import (
|
|
45
|
+
WorkerArtifact,
|
|
46
|
+
WorkerArtifactStage,
|
|
47
|
+
_AdmittedWorkerSnapshot,
|
|
48
|
+
_validated_admitted_snapshot,
|
|
49
|
+
_worker_artifact_diagnostic_source_from_snapshot,
|
|
50
|
+
_worker_reload_platform_message,
|
|
51
|
+
rebind_worker_artifact_binary,
|
|
52
|
+
stage_worker_module_instruction,
|
|
53
|
+
validate_production_worker_artifact,
|
|
54
|
+
validate_worker_export_catalog,
|
|
55
|
+
worker_artifact_stage_failure,
|
|
56
|
+
)
|
|
57
|
+
from onec_runtime.worker_stage_protocol import (
|
|
58
|
+
_temp_storage_session_id,
|
|
59
|
+
WorkerStageBatch,
|
|
60
|
+
WorkerStageBatchOutcome,
|
|
61
|
+
WorkerStageRegistrationReceipt,
|
|
62
|
+
WorkerStageEntry,
|
|
63
|
+
build_worker_stage_batches,
|
|
64
|
+
parse_worker_stage_batch_outcome,
|
|
65
|
+
stage_worker_batch_instruction,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_WORKER_MODULE_OBJECT_ID = UUID("2a00a4fa-8ea9-4dc4-9de1-472044c40101")
|
|
70
|
+
_WORKER_MODULE_PROPERTY_ID = UUID("a637f77f-3840-441d-a1c3-699c8c5cb7e0")
|
|
71
|
+
_WORKER_LOCATOR_PROFILE_ID = "external-processing-object-module-8.3.27"
|
|
72
|
+
_WORKER_LOCATOR_PLATFORM_BUILD = "8.3.27.2170"
|
|
73
|
+
_REGISTRATION_RECEIPT_ADMISSION = object()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_DESCRIPTOR_PROOF_KEY = token_bytes(32)
|
|
77
|
+
_BINARY_CAPSULE_PROOF_KEY = token_bytes(32)
|
|
78
|
+
_PREPARED_ROOT_RECEIPT_PREFIX = "onec-worker-prepared-root-receipt-v1"
|
|
79
|
+
_ROOT_SWAP_RECEIPT_PREFIX = "onec-worker-root-swap-receipt-v1"
|
|
80
|
+
_ROOT_DISCARD_RECEIPT_PREFIX = "onec-worker-root-discard-receipt-v1"
|
|
81
|
+
_BSL_KEYWORDS = frozenset(KEYWORDS)
|
|
82
|
+
_FIXED_UNQUALIFIED_WORKER_IDENTIFIERS = frozenset(
|
|
83
|
+
value.casefold()
|
|
84
|
+
for value in (
|
|
85
|
+
"Результат",
|
|
86
|
+
"Контекст",
|
|
87
|
+
"ВнешниеОбработки",
|
|
88
|
+
"ПоместитьВоВременноеХранилище",
|
|
89
|
+
"Base64Значение",
|
|
90
|
+
"ОписаниеОшибки",
|
|
91
|
+
"ИнформацияОбОшибке",
|
|
92
|
+
"ПодробноеПредставлениеОшибки",
|
|
93
|
+
"СтрДлина",
|
|
94
|
+
"Символы",
|
|
95
|
+
"ТипЗнч",
|
|
96
|
+
"Тип",
|
|
97
|
+
"Соответствие",
|
|
98
|
+
"ФиксированноеСоответствие",
|
|
99
|
+
"Массив",
|
|
100
|
+
"ФиксированныйМассив",
|
|
101
|
+
"Структура",
|
|
102
|
+
"ФиксированнаяСтруктура",
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _NotebookArtifactBuilder(Protocol):
|
|
108
|
+
def __call__(
|
|
109
|
+
self,
|
|
110
|
+
source: MappedSource,
|
|
111
|
+
exports: tuple[WorkerExport, ...],
|
|
112
|
+
*,
|
|
113
|
+
visible_source_context: VisibleSourceContext,
|
|
114
|
+
semantic_admission: WorkerSemanticAdmission | None = None,
|
|
115
|
+
semantic_packer_identity: str | None = None,
|
|
116
|
+
) -> WorkerArtifact: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class WorkerModuleBinaryKey:
|
|
121
|
+
logical_name: str
|
|
122
|
+
lowered_source_sha256: str
|
|
123
|
+
dependency_bindings_sha256: str
|
|
124
|
+
transform_version: str
|
|
125
|
+
packer_version: str
|
|
126
|
+
target_profile: str
|
|
127
|
+
|
|
128
|
+
def __post_init__(self) -> None:
|
|
129
|
+
if (
|
|
130
|
+
not _safe_identity(self.logical_name)
|
|
131
|
+
or not _sha256_value(self.lowered_source_sha256)
|
|
132
|
+
or not _sha256_value(self.dependency_bindings_sha256)
|
|
133
|
+
or not _safe_identity(self.transform_version)
|
|
134
|
+
or not _safe_identity(self.packer_version)
|
|
135
|
+
or not _safe_identity(self.target_profile)
|
|
136
|
+
):
|
|
137
|
+
raise ValueError("worker module binary key is invalid")
|
|
138
|
+
object.__setattr__(self, "logical_name", self.logical_name.casefold())
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def create(
|
|
142
|
+
cls,
|
|
143
|
+
lowered: LoweredWorkerModule,
|
|
144
|
+
*,
|
|
145
|
+
packer_version: str,
|
|
146
|
+
target_profile: str,
|
|
147
|
+
) -> WorkerModuleBinaryKey:
|
|
148
|
+
if not isinstance(lowered, LoweredWorkerModule):
|
|
149
|
+
raise TypeError("lowered worker module is required")
|
|
150
|
+
return cls(
|
|
151
|
+
lowered.analysis.unit.logical_name,
|
|
152
|
+
lowered.mapped_source.artifact.source_sha256,
|
|
153
|
+
lowered.dependency_bindings_sha256,
|
|
154
|
+
lowered.transform_version,
|
|
155
|
+
packer_version,
|
|
156
|
+
target_profile,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, slots=True, repr=False, weakref_slot=True, eq=False)
|
|
161
|
+
class WorkerModuleArtifact:
|
|
162
|
+
logical_name: str
|
|
163
|
+
revision: int
|
|
164
|
+
source_sha256: str
|
|
165
|
+
binary_key: WorkerModuleBinaryKey
|
|
166
|
+
artifact_sha256: str
|
|
167
|
+
dependency_bindings: tuple[ModuleDependencyBinding, ...]
|
|
168
|
+
exports: tuple[WorkerExport, ...]
|
|
169
|
+
source_map_sha256: str
|
|
170
|
+
worker_artifact: WorkerArtifact
|
|
171
|
+
|
|
172
|
+
def __repr__(self) -> str:
|
|
173
|
+
return (
|
|
174
|
+
"WorkerModuleArtifact("
|
|
175
|
+
f"logical_name={self.logical_name!r}, revision={self.revision}, "
|
|
176
|
+
f"artifact_sha256={self.artifact_sha256!r}, payload=<redacted>)"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
181
|
+
class _WorkerModuleBinaryCapsule:
|
|
182
|
+
key: WorkerModuleBinaryKey
|
|
183
|
+
worker_artifact: WorkerArtifact
|
|
184
|
+
proof: bytes
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class WorkerModuleArtifactCache:
|
|
188
|
+
"""Content-addressed storage for admitted, generation-independent binaries."""
|
|
189
|
+
|
|
190
|
+
__slots__ = ("_capsules",)
|
|
191
|
+
|
|
192
|
+
def __init__(self) -> None:
|
|
193
|
+
self._capsules: dict[WorkerModuleBinaryKey, _WorkerModuleBinaryCapsule] = {}
|
|
194
|
+
|
|
195
|
+
def __len__(self) -> int:
|
|
196
|
+
return len(self._capsules)
|
|
197
|
+
|
|
198
|
+
def _admit_capsule(self, capsule: _WorkerModuleBinaryCapsule) -> None:
|
|
199
|
+
try:
|
|
200
|
+
if not isinstance(capsule, _WorkerModuleBinaryCapsule):
|
|
201
|
+
raise ProtocolError("Worker binary cache admission is invalid")
|
|
202
|
+
key = capsule.key
|
|
203
|
+
worker_artifact = capsule.worker_artifact
|
|
204
|
+
validate_production_worker_artifact(worker_artifact)
|
|
205
|
+
if (
|
|
206
|
+
not compare_digest(
|
|
207
|
+
capsule.proof,
|
|
208
|
+
_binary_capsule_proof(key, worker_artifact),
|
|
209
|
+
)
|
|
210
|
+
or worker_artifact.logical_name.casefold()
|
|
211
|
+
!= key.logical_name.casefold()
|
|
212
|
+
or worker_artifact.source_sha256 != key.lowered_source_sha256
|
|
213
|
+
or not _exports_match_receiver(
|
|
214
|
+
key.logical_name,
|
|
215
|
+
worker_artifact.exports,
|
|
216
|
+
)
|
|
217
|
+
):
|
|
218
|
+
raise ProtocolError("Worker binary cache admission is invalid")
|
|
219
|
+
existing = self._capsules.get(key)
|
|
220
|
+
if existing is not None:
|
|
221
|
+
validate_production_worker_artifact(existing.worker_artifact)
|
|
222
|
+
if (
|
|
223
|
+
not compare_digest(
|
|
224
|
+
existing.proof,
|
|
225
|
+
_binary_capsule_proof(
|
|
226
|
+
existing.key,
|
|
227
|
+
existing.worker_artifact,
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
or existing.key != key
|
|
231
|
+
or existing.worker_artifact.artifact_sha256
|
|
232
|
+
!= worker_artifact.artifact_sha256
|
|
233
|
+
):
|
|
234
|
+
raise ProtocolError("Worker binary cache admission is invalid")
|
|
235
|
+
return
|
|
236
|
+
self._capsules[key] = capsule
|
|
237
|
+
except ProtocolError:
|
|
238
|
+
raise
|
|
239
|
+
except Exception:
|
|
240
|
+
raise ProtocolError("Worker binary cache admission is invalid") from None
|
|
241
|
+
|
|
242
|
+
def _lookup(self, key: WorkerModuleBinaryKey) -> WorkerArtifact | None:
|
|
243
|
+
capsule = self._capsules.get(key)
|
|
244
|
+
if capsule is None:
|
|
245
|
+
return None
|
|
246
|
+
try:
|
|
247
|
+
if capsule.key != key:
|
|
248
|
+
raise ProtocolError("Worker binary cache admission is invalid")
|
|
249
|
+
validate_production_worker_artifact(capsule.worker_artifact)
|
|
250
|
+
if (
|
|
251
|
+
not compare_digest(
|
|
252
|
+
capsule.proof,
|
|
253
|
+
_binary_capsule_proof(key, capsule.worker_artifact),
|
|
254
|
+
)
|
|
255
|
+
or capsule.worker_artifact.logical_name.casefold()
|
|
256
|
+
!= key.logical_name.casefold()
|
|
257
|
+
or capsule.worker_artifact.source_sha256
|
|
258
|
+
!= key.lowered_source_sha256
|
|
259
|
+
or not _exports_match_receiver(
|
|
260
|
+
key.logical_name,
|
|
261
|
+
capsule.worker_artifact.exports,
|
|
262
|
+
)
|
|
263
|
+
):
|
|
264
|
+
raise ProtocolError("Worker binary cache admission is invalid")
|
|
265
|
+
return capsule.worker_artifact
|
|
266
|
+
except ProtocolError:
|
|
267
|
+
raise
|
|
268
|
+
except Exception:
|
|
269
|
+
raise ProtocolError("Worker binary cache admission is invalid") from None
|
|
270
|
+
|
|
271
|
+
def _prune(self, live_keys: frozenset[WorkerModuleBinaryKey]) -> None:
|
|
272
|
+
"""Drop capsules not reachable from a confirmed generation inventory."""
|
|
273
|
+
if (
|
|
274
|
+
type(live_keys) is not frozenset
|
|
275
|
+
or any(not isinstance(key, WorkerModuleBinaryKey) for key in live_keys)
|
|
276
|
+
):
|
|
277
|
+
raise TypeError("worker binary cache live keys are invalid")
|
|
278
|
+
self._capsules = {
|
|
279
|
+
key: capsule
|
|
280
|
+
for key, capsule in self._capsules.items()
|
|
281
|
+
if key in live_keys
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class WorkerModuleArtifactBuilder:
|
|
286
|
+
__slots__ = (
|
|
287
|
+
"_artifact_builder",
|
|
288
|
+
"_cache",
|
|
289
|
+
"_packer_version",
|
|
290
|
+
"_target_profile",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
artifact_builder: _NotebookArtifactBuilder,
|
|
296
|
+
*,
|
|
297
|
+
cache: WorkerModuleArtifactCache | None = None,
|
|
298
|
+
packer_version: str,
|
|
299
|
+
target_profile: str,
|
|
300
|
+
) -> None:
|
|
301
|
+
if not callable(artifact_builder):
|
|
302
|
+
raise TypeError("notebook worker artifact builder must be callable")
|
|
303
|
+
if cache is not None and not isinstance(cache, WorkerModuleArtifactCache):
|
|
304
|
+
raise TypeError("worker module artifact cache is invalid")
|
|
305
|
+
if not _safe_identity(packer_version) or not _safe_identity(target_profile):
|
|
306
|
+
raise ValueError("worker module artifact builder identity is invalid")
|
|
307
|
+
self._artifact_builder = artifact_builder
|
|
308
|
+
self._cache = cache if cache is not None else WorkerModuleArtifactCache()
|
|
309
|
+
self._packer_version = packer_version
|
|
310
|
+
self._target_profile = target_profile
|
|
311
|
+
|
|
312
|
+
def build(
|
|
313
|
+
self,
|
|
314
|
+
lowered: LoweredWorkerModule,
|
|
315
|
+
*,
|
|
316
|
+
visible_source_context: VisibleSourceContext,
|
|
317
|
+
profiler: PhaseRecorder | None = None,
|
|
318
|
+
) -> WorkerModuleArtifact:
|
|
319
|
+
try:
|
|
320
|
+
_validate_lowered_worker_module(lowered)
|
|
321
|
+
if not isinstance(visible_source_context, VisibleSourceContext):
|
|
322
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
323
|
+
key = WorkerModuleBinaryKey.create(
|
|
324
|
+
lowered,
|
|
325
|
+
packer_version=self._packer_version,
|
|
326
|
+
target_profile=self._target_profile,
|
|
327
|
+
)
|
|
328
|
+
semantic_admission = worker_semantic_admission(
|
|
329
|
+
lowered,
|
|
330
|
+
packer_identity=self._packer_version,
|
|
331
|
+
)
|
|
332
|
+
logical_name = lowered.analysis.unit.logical_name
|
|
333
|
+
exports = _qualified_exports(lowered)
|
|
334
|
+
cached = self._cache._lookup(key)
|
|
335
|
+
if cached is None:
|
|
336
|
+
package = lambda: self._artifact_builder(
|
|
337
|
+
lowered.mapped_source,
|
|
338
|
+
exports,
|
|
339
|
+
visible_source_context=visible_source_context,
|
|
340
|
+
semantic_admission=semantic_admission,
|
|
341
|
+
semantic_packer_identity=self._packer_version,
|
|
342
|
+
)
|
|
343
|
+
packed = (
|
|
344
|
+
package()
|
|
345
|
+
if profiler is None
|
|
346
|
+
else profiler.measure(
|
|
347
|
+
"epf_packaging",
|
|
348
|
+
package,
|
|
349
|
+
item_count=lambda _result: 1,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
binary = rebind_worker_artifact_binary(
|
|
353
|
+
packed,
|
|
354
|
+
logical_name=logical_name,
|
|
355
|
+
mapped=lowered.mapped_source,
|
|
356
|
+
visible_source_context=visible_source_context,
|
|
357
|
+
exports=exports,
|
|
358
|
+
)
|
|
359
|
+
self._cache._admit_capsule(_mint_binary_capsule(key, binary))
|
|
360
|
+
worker_artifact = binary
|
|
361
|
+
else:
|
|
362
|
+
worker_artifact = rebind_worker_artifact_binary(
|
|
363
|
+
cached,
|
|
364
|
+
logical_name=logical_name,
|
|
365
|
+
mapped=lowered.mapped_source,
|
|
366
|
+
visible_source_context=visible_source_context,
|
|
367
|
+
exports=exports,
|
|
368
|
+
)
|
|
369
|
+
artifact = WorkerModuleArtifact(
|
|
370
|
+
logical_name=logical_name,
|
|
371
|
+
revision=lowered.analysis.unit.revision,
|
|
372
|
+
source_sha256=key.lowered_source_sha256,
|
|
373
|
+
binary_key=key,
|
|
374
|
+
artifact_sha256=worker_artifact.artifact_sha256,
|
|
375
|
+
dependency_bindings=lowered.analysis.dependencies,
|
|
376
|
+
exports=exports,
|
|
377
|
+
source_map_sha256=worker_artifact.source_map_sha256 or "",
|
|
378
|
+
worker_artifact=worker_artifact,
|
|
379
|
+
)
|
|
380
|
+
unit = lowered.analysis.unit
|
|
381
|
+
_bind_descriptor_admission(
|
|
382
|
+
artifact,
|
|
383
|
+
descriptor_cache_identity=(
|
|
384
|
+
unit.logical_name.casefold(),
|
|
385
|
+
unit.kind,
|
|
386
|
+
unit.revision,
|
|
387
|
+
unit.mapped_source.artifact.source_sha256,
|
|
388
|
+
unit.mapped_source.source_map_sha256,
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
return artifact
|
|
392
|
+
except Exception:
|
|
393
|
+
raise ProtocolError("Worker module artifact admission is invalid") from None
|
|
394
|
+
|
|
395
|
+
def _prune_cache(self, live_keys: frozenset[WorkerModuleBinaryKey]) -> None:
|
|
396
|
+
self._cache._prune(live_keys)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class _WorkerModuleArtifactAdmission:
|
|
400
|
+
__slots__ = ("proof", "compatibility", "descriptor_cache_identity")
|
|
401
|
+
|
|
402
|
+
def __init__(
|
|
403
|
+
self,
|
|
404
|
+
proof: bytes,
|
|
405
|
+
*,
|
|
406
|
+
compatibility: bool = False,
|
|
407
|
+
descriptor_cache_identity: tuple[str, str, int, str, str] | None = None,
|
|
408
|
+
) -> None:
|
|
409
|
+
object.__setattr__(self, "proof", proof)
|
|
410
|
+
object.__setattr__(self, "compatibility", compatibility)
|
|
411
|
+
object.__setattr__(
|
|
412
|
+
self,
|
|
413
|
+
"descriptor_cache_identity",
|
|
414
|
+
descriptor_cache_identity,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
418
|
+
raise AttributeError("worker module artifact admissions are immutable")
|
|
419
|
+
|
|
420
|
+
def __repr__(self) -> str:
|
|
421
|
+
return "<redacted worker module artifact admission>"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
_DESCRIPTOR_ADMISSIONS: WeakKeyDictionary[
|
|
425
|
+
WorkerModuleArtifact,
|
|
426
|
+
_WorkerModuleArtifactAdmission,
|
|
427
|
+
] = WeakKeyDictionary()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _validate_descriptor_cache_identity(
|
|
431
|
+
artifact: WorkerModuleArtifact,
|
|
432
|
+
identity: tuple[str, str, int, str, str] | None,
|
|
433
|
+
*,
|
|
434
|
+
compatibility: bool,
|
|
435
|
+
) -> None:
|
|
436
|
+
if compatibility:
|
|
437
|
+
if identity is not None:
|
|
438
|
+
raise ProtocolError("Worker module descriptor cache identity is invalid")
|
|
439
|
+
return
|
|
440
|
+
if (
|
|
441
|
+
not isinstance(identity, tuple)
|
|
442
|
+
or len(identity) != 5
|
|
443
|
+
or identity[0] != artifact.logical_name.casefold()
|
|
444
|
+
or identity[1] not in ("module", "test-module")
|
|
445
|
+
or identity[2] != artifact.revision
|
|
446
|
+
or not _sha256_value(identity[3])
|
|
447
|
+
or not _sha256_value(identity[4])
|
|
448
|
+
):
|
|
449
|
+
raise ProtocolError("Worker module descriptor cache identity is invalid")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _worker_module_descriptor_cache_identity(
|
|
453
|
+
artifact: WorkerModuleArtifact,
|
|
454
|
+
) -> tuple[str, str, int, str, str] | None:
|
|
455
|
+
admission = _DESCRIPTOR_ADMISSIONS.get(artifact)
|
|
456
|
+
if admission is None:
|
|
457
|
+
raise ProtocolError("Worker module descriptor cache identity is invalid")
|
|
458
|
+
identity = admission.descriptor_cache_identity
|
|
459
|
+
_validate_descriptor_cache_identity(
|
|
460
|
+
artifact,
|
|
461
|
+
identity,
|
|
462
|
+
compatibility=admission.compatibility,
|
|
463
|
+
)
|
|
464
|
+
return identity
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _bind_descriptor_admission(
|
|
468
|
+
artifact: WorkerModuleArtifact,
|
|
469
|
+
*,
|
|
470
|
+
compatibility: bool = False,
|
|
471
|
+
descriptor_cache_identity: tuple[str, str, int, str, str] | None = None,
|
|
472
|
+
) -> None:
|
|
473
|
+
_validate_descriptor_cache_identity(
|
|
474
|
+
artifact,
|
|
475
|
+
descriptor_cache_identity,
|
|
476
|
+
compatibility=compatibility,
|
|
477
|
+
)
|
|
478
|
+
_DESCRIPTOR_ADMISSIONS[artifact] = _WorkerModuleArtifactAdmission(
|
|
479
|
+
_descriptor_proof(artifact, compatibility=compatibility),
|
|
480
|
+
compatibility=compatibility,
|
|
481
|
+
descriptor_cache_identity=descriptor_cache_identity,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def worker_module_artifact_from_notebook(
|
|
486
|
+
artifact: WorkerArtifact,
|
|
487
|
+
*,
|
|
488
|
+
revision: int,
|
|
489
|
+
target_profile: str,
|
|
490
|
+
) -> WorkerModuleArtifact:
|
|
491
|
+
"""Admit a current notebook Worker binary into the module universe.
|
|
492
|
+
|
|
493
|
+
The returned descriptor retains the exact admitted binary and mapped source;
|
|
494
|
+
only its host-side export descriptors gain the canonical ``Worker`` receiver.
|
|
495
|
+
"""
|
|
496
|
+
try:
|
|
497
|
+
catalog = validate_production_worker_artifact(artifact)
|
|
498
|
+
snapshot = _validated_admitted_snapshot(artifact)
|
|
499
|
+
mapped = snapshot.mapped_source
|
|
500
|
+
if (
|
|
501
|
+
artifact.logical_name.casefold() != "worker"
|
|
502
|
+
or type(revision) is not int
|
|
503
|
+
or revision <= 0
|
|
504
|
+
or not _safe_identity(target_profile)
|
|
505
|
+
or any(item.receiver_module is not None for item in catalog)
|
|
506
|
+
):
|
|
507
|
+
raise ProtocolError("Notebook Worker admission is invalid")
|
|
508
|
+
exports = tuple(
|
|
509
|
+
WorkerExport(
|
|
510
|
+
f"Worker.{item.method}",
|
|
511
|
+
item.method,
|
|
512
|
+
receiver_module="Worker",
|
|
513
|
+
)
|
|
514
|
+
for item in catalog
|
|
515
|
+
)
|
|
516
|
+
validate_worker_export_catalog(exports, require_nonempty=True)
|
|
517
|
+
dependency_sha256 = _dependency_bindings_sha256(
|
|
518
|
+
(),
|
|
519
|
+
validate_bindings=True,
|
|
520
|
+
)
|
|
521
|
+
key = WorkerModuleBinaryKey(
|
|
522
|
+
"Worker",
|
|
523
|
+
artifact.source_sha256,
|
|
524
|
+
dependency_sha256,
|
|
525
|
+
"notebook-worker-adapter-v1",
|
|
526
|
+
"notebook-admitted-binary-v1",
|
|
527
|
+
target_profile,
|
|
528
|
+
)
|
|
529
|
+
descriptor = WorkerModuleArtifact(
|
|
530
|
+
logical_name="Worker",
|
|
531
|
+
revision=revision,
|
|
532
|
+
source_sha256=artifact.source_sha256,
|
|
533
|
+
binary_key=key,
|
|
534
|
+
artifact_sha256=artifact.artifact_sha256,
|
|
535
|
+
dependency_bindings=(),
|
|
536
|
+
exports=exports,
|
|
537
|
+
source_map_sha256=snapshot.source_map_sha256,
|
|
538
|
+
worker_artifact=artifact,
|
|
539
|
+
)
|
|
540
|
+
_bind_descriptor_admission(descriptor, compatibility=True)
|
|
541
|
+
return descriptor
|
|
542
|
+
except ProtocolError as error:
|
|
543
|
+
if str(error) == "Notebook Worker admission is invalid":
|
|
544
|
+
raise
|
|
545
|
+
raise ProtocolError("Notebook Worker admission is invalid") from None
|
|
546
|
+
except Exception:
|
|
547
|
+
raise ProtocolError("Notebook Worker admission is invalid") from None
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _mint_binary_capsule(
|
|
551
|
+
key: WorkerModuleBinaryKey,
|
|
552
|
+
worker_artifact: WorkerArtifact,
|
|
553
|
+
) -> _WorkerModuleBinaryCapsule:
|
|
554
|
+
validate_production_worker_artifact(worker_artifact)
|
|
555
|
+
return _WorkerModuleBinaryCapsule(
|
|
556
|
+
key,
|
|
557
|
+
worker_artifact,
|
|
558
|
+
_binary_capsule_proof(key, worker_artifact),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _binary_capsule_proof(
|
|
563
|
+
key: WorkerModuleBinaryKey,
|
|
564
|
+
worker_artifact: WorkerArtifact,
|
|
565
|
+
) -> bytes:
|
|
566
|
+
artifact_proof = _validated_admitted_snapshot(worker_artifact).proof
|
|
567
|
+
manifest = {
|
|
568
|
+
"schema": "onec-worker-module-binary-capsule-v1",
|
|
569
|
+
"binary_key": {
|
|
570
|
+
"logical_name": key.logical_name,
|
|
571
|
+
"lowered_source_sha256": key.lowered_source_sha256,
|
|
572
|
+
"dependency_bindings_sha256": key.dependency_bindings_sha256,
|
|
573
|
+
"transform_version": key.transform_version,
|
|
574
|
+
"packer_version": key.packer_version,
|
|
575
|
+
"target_profile": key.target_profile,
|
|
576
|
+
},
|
|
577
|
+
"worker_artifact": {
|
|
578
|
+
"logical_name": worker_artifact.logical_name.casefold(),
|
|
579
|
+
"source_sha256": worker_artifact.source_sha256,
|
|
580
|
+
"artifact_sha256": worker_artifact.artifact_sha256,
|
|
581
|
+
"source_map_sha256": worker_artifact.source_map_sha256,
|
|
582
|
+
"admission_proof_hex": artifact_proof.hex(),
|
|
583
|
+
},
|
|
584
|
+
}
|
|
585
|
+
payload = json.dumps(
|
|
586
|
+
manifest,
|
|
587
|
+
ensure_ascii=False,
|
|
588
|
+
sort_keys=True,
|
|
589
|
+
separators=(",", ":"),
|
|
590
|
+
).encode("utf-8")
|
|
591
|
+
return digest(
|
|
592
|
+
_BINARY_CAPSULE_PROOF_KEY,
|
|
593
|
+
payload,
|
|
594
|
+
"sha256",
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
599
|
+
class _ValidatedWorkerModuleArtifact:
|
|
600
|
+
"""Private result of one complete revision-descriptor admission."""
|
|
601
|
+
|
|
602
|
+
descriptor: WorkerModuleArtifact
|
|
603
|
+
snapshot: _AdmittedWorkerSnapshot
|
|
604
|
+
descriptor_cache_identity: tuple[str, str, int, str, str] | None
|
|
605
|
+
|
|
606
|
+
def __repr__(self) -> str:
|
|
607
|
+
return "<validated worker module artifact>"
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _validated_worker_module_artifact(
|
|
611
|
+
artifact: WorkerModuleArtifact,
|
|
612
|
+
) -> _ValidatedWorkerModuleArtifact:
|
|
613
|
+
"""Fail-closed production admission for a revision descriptor and capsule."""
|
|
614
|
+
try:
|
|
615
|
+
if not isinstance(artifact, WorkerModuleArtifact):
|
|
616
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
617
|
+
admission = _DESCRIPTOR_ADMISSIONS.get(artifact)
|
|
618
|
+
if admission is None or not compare_digest(
|
|
619
|
+
admission.proof,
|
|
620
|
+
_descriptor_proof(
|
|
621
|
+
artifact,
|
|
622
|
+
compatibility=admission.compatibility,
|
|
623
|
+
),
|
|
624
|
+
):
|
|
625
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
626
|
+
descriptor_cache_identity = _worker_module_descriptor_cache_identity(artifact)
|
|
627
|
+
if admission.compatibility:
|
|
628
|
+
# The public compatibility factory performed the live-file
|
|
629
|
+
# production admission before it minted this descriptor. From
|
|
630
|
+
# here on the descriptor owns that exact immutable admission
|
|
631
|
+
# snapshot: staging must not reopen a path that may have changed
|
|
632
|
+
# after admission (the stage instruction below consumes the
|
|
633
|
+
# admitted bytes, not the path).
|
|
634
|
+
snapshot = _validated_admitted_snapshot(artifact.worker_artifact)
|
|
635
|
+
mapped = snapshot.mapped_source
|
|
636
|
+
catalog = validate_worker_export_catalog(
|
|
637
|
+
artifact.worker_artifact.exports,
|
|
638
|
+
require_nonempty=True,
|
|
639
|
+
)
|
|
640
|
+
else:
|
|
641
|
+
snapshot = _validated_admitted_snapshot(artifact.worker_artifact)
|
|
642
|
+
mapped = snapshot.mapped_source
|
|
643
|
+
catalog = snapshot.catalog
|
|
644
|
+
validate_worker_export_catalog(artifact.exports, require_nonempty=True)
|
|
645
|
+
dependency_sha256 = _dependency_bindings_sha256(
|
|
646
|
+
artifact.dependency_bindings,
|
|
647
|
+
validate_bindings=True,
|
|
648
|
+
)
|
|
649
|
+
compact_spans = getattr(mapped.source_map, "compact_visible_spans", None)
|
|
650
|
+
references = (
|
|
651
|
+
tuple(reference for _role, reference, _span in compact_spans())
|
|
652
|
+
if compact_spans is not None
|
|
653
|
+
else tuple(
|
|
654
|
+
reference
|
|
655
|
+
for segment in mapped.source_map.segments
|
|
656
|
+
for reference in (segment.origin_ref, segment.anchor_ref)
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
visible_revisions = {
|
|
660
|
+
reference.revision
|
|
661
|
+
for reference in references
|
|
662
|
+
if (
|
|
663
|
+
getattr(reference, "kind", None)
|
|
664
|
+
in (SourceUnitKind.MODULE, SourceUnitKind.TEST_MODULE)
|
|
665
|
+
and getattr(reference, "unit_id", "").casefold()
|
|
666
|
+
== artifact.logical_name.casefold()
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
compatibility_catalog = (
|
|
670
|
+
admission.compatibility
|
|
671
|
+
and artifact.logical_name == "Worker"
|
|
672
|
+
and artifact.binary_key.transform_version
|
|
673
|
+
== "notebook-worker-adapter-v1"
|
|
674
|
+
and artifact.binary_key.packer_version
|
|
675
|
+
== "notebook-admitted-binary-v1"
|
|
676
|
+
and artifact.dependency_bindings == ()
|
|
677
|
+
and len(artifact.exports) == len(catalog)
|
|
678
|
+
and all(
|
|
679
|
+
descriptor.receiver_module == "Worker"
|
|
680
|
+
and source.receiver_module is None
|
|
681
|
+
and descriptor.public_path.casefold()
|
|
682
|
+
== f"worker.{source.method}".casefold()
|
|
683
|
+
and descriptor.method.casefold() == source.method.casefold()
|
|
684
|
+
for descriptor, source in zip(
|
|
685
|
+
artifact.exports,
|
|
686
|
+
catalog,
|
|
687
|
+
strict=True,
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
if (
|
|
692
|
+
artifact.logical_name.casefold()
|
|
693
|
+
!= artifact.binary_key.logical_name.casefold()
|
|
694
|
+
or artifact.logical_name.casefold()
|
|
695
|
+
!= artifact.worker_artifact.logical_name.casefold()
|
|
696
|
+
or type(artifact.revision) is not int
|
|
697
|
+
or artifact.revision < 0
|
|
698
|
+
or (
|
|
699
|
+
not admission.compatibility
|
|
700
|
+
and visible_revisions != {artifact.revision}
|
|
701
|
+
)
|
|
702
|
+
or artifact.source_sha256
|
|
703
|
+
!= artifact.binary_key.lowered_source_sha256
|
|
704
|
+
or artifact.source_sha256 != artifact.worker_artifact.source_sha256
|
|
705
|
+
or artifact.source_sha256 != mapped.artifact.source_sha256
|
|
706
|
+
or artifact.artifact_sha256 != artifact.worker_artifact.artifact_sha256
|
|
707
|
+
or artifact.source_map_sha256 != artifact.worker_artifact.source_map_sha256
|
|
708
|
+
or artifact.source_map_sha256 != snapshot.source_map_sha256
|
|
709
|
+
or dependency_sha256
|
|
710
|
+
!= artifact.binary_key.dependency_bindings_sha256
|
|
711
|
+
or (
|
|
712
|
+
not admission.compatibility
|
|
713
|
+
and artifact.exports != artifact.worker_artifact.exports
|
|
714
|
+
)
|
|
715
|
+
or (
|
|
716
|
+
not admission.compatibility
|
|
717
|
+
and artifact.exports != catalog
|
|
718
|
+
)
|
|
719
|
+
or (admission.compatibility and not compatibility_catalog)
|
|
720
|
+
or not _exports_match_receiver(artifact.logical_name, artifact.exports)
|
|
721
|
+
):
|
|
722
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
723
|
+
return _ValidatedWorkerModuleArtifact(
|
|
724
|
+
artifact,
|
|
725
|
+
snapshot,
|
|
726
|
+
descriptor_cache_identity,
|
|
727
|
+
)
|
|
728
|
+
except ProtocolError as error:
|
|
729
|
+
if str(error) == "Worker module artifact admission is invalid":
|
|
730
|
+
raise
|
|
731
|
+
raise ProtocolError("Worker module artifact admission is invalid") from None
|
|
732
|
+
except Exception:
|
|
733
|
+
raise ProtocolError("Worker module artifact admission is invalid") from None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def validate_worker_module_artifact(
|
|
737
|
+
artifact: WorkerModuleArtifact,
|
|
738
|
+
) -> tuple[WorkerExport, ...]:
|
|
739
|
+
"""Validate one descriptor and return its public export catalog."""
|
|
740
|
+
return _validated_worker_module_artifact(artifact).descriptor.exports
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _validate_lowered_worker_module(lowered: LoweredWorkerModule) -> None:
|
|
744
|
+
if not isinstance(lowered, LoweredWorkerModule):
|
|
745
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
746
|
+
if (
|
|
747
|
+
lowered.mapped_source.artifact.worker_generation is not None
|
|
748
|
+
or lowered.mapped_source.artifact.worker_manifest_sha256 is not None
|
|
749
|
+
or not _safe_identity(lowered.transform_version)
|
|
750
|
+
or lowered.dependency_bindings_sha256
|
|
751
|
+
!= _dependency_bindings_sha256(
|
|
752
|
+
lowered.analysis.dependencies,
|
|
753
|
+
validate_bindings=True,
|
|
754
|
+
)
|
|
755
|
+
):
|
|
756
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _qualified_exports(lowered: LoweredWorkerModule) -> tuple[WorkerExport, ...]:
|
|
760
|
+
logical_name = lowered.analysis.unit.logical_name
|
|
761
|
+
exports = tuple(
|
|
762
|
+
WorkerExport(
|
|
763
|
+
f"{logical_name}.{method}",
|
|
764
|
+
method,
|
|
765
|
+
receiver_module=logical_name,
|
|
766
|
+
)
|
|
767
|
+
for method in lowered.analysis.exported_methods
|
|
768
|
+
)
|
|
769
|
+
return validate_worker_export_catalog(exports, require_nonempty=True)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _exports_match_receiver(
|
|
773
|
+
logical_name: str,
|
|
774
|
+
exports: tuple[WorkerExport, ...],
|
|
775
|
+
) -> bool:
|
|
776
|
+
return all(
|
|
777
|
+
item.receiver_module is not None
|
|
778
|
+
and item.receiver_module.casefold() == logical_name.casefold()
|
|
779
|
+
and item.public_path.casefold()
|
|
780
|
+
== f"{logical_name}.{item.method}".casefold()
|
|
781
|
+
for item in exports
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _binding_manifest(binding: ModuleDependencyBinding) -> dict[str, object]:
|
|
786
|
+
return {
|
|
787
|
+
"target_module": binding.target_module,
|
|
788
|
+
"export_variable": binding.export_variable,
|
|
789
|
+
"uses": [
|
|
790
|
+
{
|
|
791
|
+
"method_name": use.method_name,
|
|
792
|
+
"category": use.category,
|
|
793
|
+
"span": [use.span.start, use.span.end],
|
|
794
|
+
"method_declaration": [
|
|
795
|
+
use.method_declaration.start,
|
|
796
|
+
use.method_declaration.end,
|
|
797
|
+
],
|
|
798
|
+
}
|
|
799
|
+
for use in binding.uses
|
|
800
|
+
],
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _binding_sha256(binding: ModuleDependencyBinding) -> str:
|
|
805
|
+
payload = json.dumps(
|
|
806
|
+
_binding_manifest(binding),
|
|
807
|
+
ensure_ascii=False,
|
|
808
|
+
sort_keys=True,
|
|
809
|
+
separators=(",", ":"),
|
|
810
|
+
)
|
|
811
|
+
return sha256(payload.encode("utf-8")).hexdigest()
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _dependency_bindings_sha256(
|
|
815
|
+
bindings: tuple[ModuleDependencyBinding, ...],
|
|
816
|
+
*,
|
|
817
|
+
validate_bindings: bool,
|
|
818
|
+
) -> str:
|
|
819
|
+
if type(bindings) is not tuple:
|
|
820
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
821
|
+
if validate_bindings:
|
|
822
|
+
for binding in bindings:
|
|
823
|
+
if (
|
|
824
|
+
not isinstance(binding, ModuleDependencyBinding)
|
|
825
|
+
or type(binding.uses) is not tuple
|
|
826
|
+
or any(not isinstance(use, DependencyUse) for use in binding.uses)
|
|
827
|
+
or binding.sha256 != _binding_sha256(binding)
|
|
828
|
+
):
|
|
829
|
+
raise ProtocolError("Worker module artifact admission is invalid")
|
|
830
|
+
payload = json.dumps(
|
|
831
|
+
[binding.sha256 for binding in bindings],
|
|
832
|
+
ensure_ascii=False,
|
|
833
|
+
separators=(",", ":"),
|
|
834
|
+
)
|
|
835
|
+
return sha256(payload.encode("utf-8")).hexdigest()
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _descriptor_proof(
|
|
839
|
+
artifact: WorkerModuleArtifact,
|
|
840
|
+
*,
|
|
841
|
+
compatibility: bool = False,
|
|
842
|
+
) -> bytes:
|
|
843
|
+
binding_payload = json.dumps(
|
|
844
|
+
[_binding_manifest(binding) | {"sha256": binding.sha256} for binding in artifact.dependency_bindings],
|
|
845
|
+
ensure_ascii=False,
|
|
846
|
+
sort_keys=True,
|
|
847
|
+
separators=(",", ":"),
|
|
848
|
+
)
|
|
849
|
+
export_payload = json.dumps(
|
|
850
|
+
[
|
|
851
|
+
[item.public_path, item.method, item.receiver_module]
|
|
852
|
+
for item in artifact.exports
|
|
853
|
+
],
|
|
854
|
+
ensure_ascii=False,
|
|
855
|
+
separators=(",", ":"),
|
|
856
|
+
)
|
|
857
|
+
key_payload = json.dumps(
|
|
858
|
+
[
|
|
859
|
+
artifact.binary_key.logical_name,
|
|
860
|
+
artifact.binary_key.lowered_source_sha256,
|
|
861
|
+
artifact.binary_key.dependency_bindings_sha256,
|
|
862
|
+
artifact.binary_key.transform_version,
|
|
863
|
+
artifact.binary_key.packer_version,
|
|
864
|
+
artifact.binary_key.target_profile,
|
|
865
|
+
],
|
|
866
|
+
ensure_ascii=False,
|
|
867
|
+
separators=(",", ":"),
|
|
868
|
+
)
|
|
869
|
+
fields = (
|
|
870
|
+
"compatibility" if compatibility else "module",
|
|
871
|
+
artifact.logical_name,
|
|
872
|
+
str(artifact.revision),
|
|
873
|
+
artifact.source_sha256,
|
|
874
|
+
key_payload,
|
|
875
|
+
artifact.artifact_sha256,
|
|
876
|
+
binding_payload,
|
|
877
|
+
export_payload,
|
|
878
|
+
artifact.source_map_sha256,
|
|
879
|
+
artifact.worker_artifact.logical_name,
|
|
880
|
+
artifact.worker_artifact.source_sha256,
|
|
881
|
+
artifact.worker_artifact.artifact_sha256,
|
|
882
|
+
artifact.worker_artifact.source_map_sha256 or "",
|
|
883
|
+
)
|
|
884
|
+
return digest(
|
|
885
|
+
_DESCRIPTOR_PROOF_KEY,
|
|
886
|
+
"\x1f".join(fields).encode("utf-8"),
|
|
887
|
+
"sha256",
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _sha256_value(value: object) -> bool:
|
|
892
|
+
return isinstance(value, str) and fullmatch(r"[0-9a-f]{64}", value) is not None
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _safe_identity(value: object) -> bool:
|
|
896
|
+
return (
|
|
897
|
+
isinstance(value, str)
|
|
898
|
+
and bool(value)
|
|
899
|
+
and "\x00" not in value
|
|
900
|
+
and "\r" not in value
|
|
901
|
+
and "\n" not in value
|
|
902
|
+
and "/" not in value
|
|
903
|
+
and "\\" not in value
|
|
904
|
+
and "|" not in value
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
class WorkerUniverseState(Enum):
|
|
909
|
+
EMPTY = "empty"
|
|
910
|
+
READY = "ready"
|
|
911
|
+
PREPARING = "preparing"
|
|
912
|
+
BROKEN = "broken"
|
|
913
|
+
CLOSED = "closed"
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@dataclass(frozen=True, slots=True)
|
|
917
|
+
class ManifestModule:
|
|
918
|
+
logical_name: str
|
|
919
|
+
revision: int
|
|
920
|
+
artifact_sha256: str
|
|
921
|
+
registration_name: str
|
|
922
|
+
|
|
923
|
+
def __post_init__(self) -> None:
|
|
924
|
+
if (
|
|
925
|
+
not _bsl_identifier(self.logical_name)
|
|
926
|
+
or type(self.revision) is not int
|
|
927
|
+
or self.revision < 0
|
|
928
|
+
or not _sha256_value(self.artifact_sha256)
|
|
929
|
+
or self.registration_name
|
|
930
|
+
!= registration_name(self.logical_name, self.artifact_sha256)
|
|
931
|
+
):
|
|
932
|
+
raise ValueError("worker manifest module is invalid")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
@dataclass(frozen=True, slots=True)
|
|
936
|
+
class DependencyTarget:
|
|
937
|
+
source_module: str
|
|
938
|
+
export_variable: str
|
|
939
|
+
target_kind: Literal["original", "overloaded"]
|
|
940
|
+
target_module: str
|
|
941
|
+
|
|
942
|
+
def __post_init__(self) -> None:
|
|
943
|
+
if not _valid_dependency_target(self):
|
|
944
|
+
raise ValueError("worker dependency target is invalid")
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
@dataclass(frozen=True, slots=True)
|
|
948
|
+
class WorkerUniverseManifest:
|
|
949
|
+
generation: int
|
|
950
|
+
modules: tuple[ManifestModule, ...]
|
|
951
|
+
wiring: tuple[DependencyTarget, ...]
|
|
952
|
+
exports: tuple[WorkerExport, ...]
|
|
953
|
+
sha256: str
|
|
954
|
+
|
|
955
|
+
def __post_init__(self) -> None:
|
|
956
|
+
if (
|
|
957
|
+
type(self.generation) is not int
|
|
958
|
+
or self.generation <= 0
|
|
959
|
+
or type(self.modules) is not tuple
|
|
960
|
+
or not self.modules
|
|
961
|
+
or any(not isinstance(item, ManifestModule) for item in self.modules)
|
|
962
|
+
or self.modules != tuple(sorted(self.modules, key=_manifest_module_sort_key))
|
|
963
|
+
or type(self.wiring) is not tuple
|
|
964
|
+
or any(not isinstance(item, DependencyTarget) for item in self.wiring)
|
|
965
|
+
or any(not _valid_dependency_target(item) for item in self.wiring)
|
|
966
|
+
or self.wiring != tuple(sorted(self.wiring, key=_dependency_target_sort_key))
|
|
967
|
+
or type(self.exports) is not tuple
|
|
968
|
+
or self.exports != tuple(sorted(self.exports, key=_worker_export_sort_key))
|
|
969
|
+
or any(
|
|
970
|
+
not _bsl_identifier(item.method)
|
|
971
|
+
or not _bsl_identifier(item.receiver_module)
|
|
972
|
+
for item in self.exports
|
|
973
|
+
)
|
|
974
|
+
or not _sha256_value(self.sha256)
|
|
975
|
+
):
|
|
976
|
+
raise ValueError("worker universe manifest is invalid")
|
|
977
|
+
module_names = tuple(item.logical_name.casefold() for item in self.modules)
|
|
978
|
+
registration_names = tuple(
|
|
979
|
+
item.registration_name.casefold() for item in self.modules
|
|
980
|
+
)
|
|
981
|
+
if (
|
|
982
|
+
len(module_names) != len(set(module_names))
|
|
983
|
+
or len(registration_names) != len(set(registration_names))
|
|
984
|
+
):
|
|
985
|
+
raise ValueError("worker universe manifest is invalid")
|
|
986
|
+
binding_keys = tuple(
|
|
987
|
+
(item.source_module.casefold(), item.export_variable.casefold())
|
|
988
|
+
for item in self.wiring
|
|
989
|
+
)
|
|
990
|
+
if len(binding_keys) != len(set(binding_keys)):
|
|
991
|
+
raise ValueError("worker universe manifest is invalid")
|
|
992
|
+
loaded = set(module_names)
|
|
993
|
+
if any(
|
|
994
|
+
item.source_module.casefold() not in loaded
|
|
995
|
+
or (
|
|
996
|
+
item.target_kind == "overloaded"
|
|
997
|
+
and item.target_module.casefold() not in loaded
|
|
998
|
+
)
|
|
999
|
+
or (
|
|
1000
|
+
item.target_kind == "original"
|
|
1001
|
+
and item.target_module.casefold() in loaded
|
|
1002
|
+
)
|
|
1003
|
+
for item in self.wiring
|
|
1004
|
+
):
|
|
1005
|
+
raise ValueError("worker universe manifest is invalid")
|
|
1006
|
+
try:
|
|
1007
|
+
validate_worker_export_catalog(self.exports, require_nonempty=True)
|
|
1008
|
+
except (ProtocolError, ValueError) as error:
|
|
1009
|
+
raise ValueError("worker universe manifest is invalid") from error
|
|
1010
|
+
if any(
|
|
1011
|
+
item.receiver_module is None
|
|
1012
|
+
or item.receiver_module.casefold() not in loaded
|
|
1013
|
+
for item in self.exports
|
|
1014
|
+
):
|
|
1015
|
+
raise ValueError("worker universe manifest is invalid")
|
|
1016
|
+
if self.sha256 != _worker_manifest_sha256(
|
|
1017
|
+
self.generation,
|
|
1018
|
+
self.modules,
|
|
1019
|
+
self.wiring,
|
|
1020
|
+
self.exports,
|
|
1021
|
+
):
|
|
1022
|
+
raise ValueError("worker universe manifest is invalid")
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
1026
|
+
class WorkerGenerationHandle:
|
|
1027
|
+
runtime_generation: int
|
|
1028
|
+
context_generation: int
|
|
1029
|
+
generation: int
|
|
1030
|
+
manifest_sha256: str
|
|
1031
|
+
|
|
1032
|
+
def __post_init__(self) -> None:
|
|
1033
|
+
if (
|
|
1034
|
+
type(self.runtime_generation) is not int
|
|
1035
|
+
or self.runtime_generation <= 0
|
|
1036
|
+
or type(self.context_generation) is not int
|
|
1037
|
+
or self.context_generation <= 0
|
|
1038
|
+
or type(self.generation) is not int
|
|
1039
|
+
or self.generation <= 0
|
|
1040
|
+
or not _sha256_value(self.manifest_sha256)
|
|
1041
|
+
):
|
|
1042
|
+
raise ValueError("worker generation handle is invalid")
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
1046
|
+
class WorkerGenerationConfirmation:
|
|
1047
|
+
handle: WorkerGenerationHandle
|
|
1048
|
+
released_registrations: tuple[str, ...]
|
|
1049
|
+
|
|
1050
|
+
def __post_init__(self) -> None:
|
|
1051
|
+
if (
|
|
1052
|
+
not isinstance(self.handle, WorkerGenerationHandle)
|
|
1053
|
+
or type(self.released_registrations) is not tuple
|
|
1054
|
+
or any(
|
|
1055
|
+
not _bsl_identifier(name)
|
|
1056
|
+
for name in self.released_registrations
|
|
1057
|
+
)
|
|
1058
|
+
or self.released_registrations
|
|
1059
|
+
!= tuple(sorted(self.released_registrations, key=str.casefold))
|
|
1060
|
+
or len(self.released_registrations)
|
|
1061
|
+
!= len({name.casefold() for name in self.released_registrations})
|
|
1062
|
+
):
|
|
1063
|
+
raise ValueError("worker generation confirmation is invalid")
|
|
1064
|
+
|
|
1065
|
+
def __repr__(self) -> str:
|
|
1066
|
+
return (
|
|
1067
|
+
"WorkerGenerationConfirmation("
|
|
1068
|
+
"handle=<redacted>, released_registrations=<redacted>, "
|
|
1069
|
+
f"released_count={len(self.released_registrations)})"
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
@dataclass(frozen=True, slots=True, eq=False, repr=False)
|
|
1074
|
+
class OperationGenerationPin:
|
|
1075
|
+
handle: WorkerGenerationHandle
|
|
1076
|
+
manifest: WorkerUniverseManifest
|
|
1077
|
+
root_key: str
|
|
1078
|
+
export_catalog: tuple[WorkerExport, ...]
|
|
1079
|
+
lease_id: str
|
|
1080
|
+
|
|
1081
|
+
def __post_init__(self) -> None:
|
|
1082
|
+
if (
|
|
1083
|
+
not isinstance(self.handle, WorkerGenerationHandle)
|
|
1084
|
+
or not isinstance(self.manifest, WorkerUniverseManifest)
|
|
1085
|
+
or self.handle.generation != self.manifest.generation
|
|
1086
|
+
or self.handle.manifest_sha256 != self.manifest.sha256
|
|
1087
|
+
or not _safe_identity(self.root_key)
|
|
1088
|
+
or type(self.export_catalog) is not tuple
|
|
1089
|
+
or not _effective_export_catalog_matches(
|
|
1090
|
+
self.manifest,
|
|
1091
|
+
self.export_catalog,
|
|
1092
|
+
)
|
|
1093
|
+
or not isinstance(self.lease_id, str)
|
|
1094
|
+
or len(self.lease_id) < 32
|
|
1095
|
+
):
|
|
1096
|
+
raise ValueError("worker operation generation pin is invalid")
|
|
1097
|
+
|
|
1098
|
+
def __repr__(self) -> str:
|
|
1099
|
+
return (
|
|
1100
|
+
"OperationGenerationPin("
|
|
1101
|
+
f"handle={self.handle!r}, root_key=<redacted>, "
|
|
1102
|
+
f"exports={len(self.export_catalog)}, lease_id=<redacted>)"
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
@dataclass(frozen=True, slots=True, eq=False, repr=False)
|
|
1107
|
+
class WorkerUniverseCandidate:
|
|
1108
|
+
handle: WorkerGenerationHandle
|
|
1109
|
+
manifest: WorkerUniverseManifest
|
|
1110
|
+
export_catalog: tuple[WorkerExport, ...]
|
|
1111
|
+
artifacts: tuple[WorkerModuleArtifact, ...]
|
|
1112
|
+
new_artifacts: tuple[WorkerModuleArtifact, ...]
|
|
1113
|
+
previous: WorkerGenerationHandle | None
|
|
1114
|
+
|
|
1115
|
+
def __post_init__(self) -> None:
|
|
1116
|
+
if (
|
|
1117
|
+
not isinstance(self.handle, WorkerGenerationHandle)
|
|
1118
|
+
or not isinstance(self.manifest, WorkerUniverseManifest)
|
|
1119
|
+
or self.handle.generation != self.manifest.generation
|
|
1120
|
+
or self.handle.manifest_sha256 != self.manifest.sha256
|
|
1121
|
+
or not _effective_export_catalog_matches(
|
|
1122
|
+
self.manifest,
|
|
1123
|
+
self.export_catalog,
|
|
1124
|
+
)
|
|
1125
|
+
or type(self.artifacts) is not tuple
|
|
1126
|
+
or not self.artifacts
|
|
1127
|
+
or any(not isinstance(item, WorkerModuleArtifact) for item in self.artifacts)
|
|
1128
|
+
or type(self.new_artifacts) is not tuple
|
|
1129
|
+
or any(
|
|
1130
|
+
not any(item is artifact for artifact in self.artifacts)
|
|
1131
|
+
for item in self.new_artifacts
|
|
1132
|
+
)
|
|
1133
|
+
or (
|
|
1134
|
+
self.previous is not None
|
|
1135
|
+
and not isinstance(self.previous, WorkerGenerationHandle)
|
|
1136
|
+
)
|
|
1137
|
+
):
|
|
1138
|
+
raise ValueError("worker universe candidate is invalid")
|
|
1139
|
+
|
|
1140
|
+
def __repr__(self) -> str:
|
|
1141
|
+
return (
|
|
1142
|
+
"WorkerUniverseCandidate("
|
|
1143
|
+
f"handle={self.handle!r}, modules={len(self.artifacts)}, "
|
|
1144
|
+
f"new_artifacts={len(self.new_artifacts)}, payload=<redacted>)"
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@dataclass(frozen=True, slots=True, eq=False, repr=False)
|
|
1149
|
+
class _SealedWorkerStageEntry:
|
|
1150
|
+
"""Exact target payload and diagnostics retained for one sealed module."""
|
|
1151
|
+
|
|
1152
|
+
descriptor: WorkerModuleArtifact
|
|
1153
|
+
logical_name: str
|
|
1154
|
+
revision: int
|
|
1155
|
+
artifact_sha256: str
|
|
1156
|
+
registration_name: str
|
|
1157
|
+
artifact_bytes: bytes
|
|
1158
|
+
admitted_snapshot: _AdmittedWorkerSnapshot
|
|
1159
|
+
|
|
1160
|
+
def __repr__(self) -> str:
|
|
1161
|
+
return (
|
|
1162
|
+
"_SealedWorkerStageEntry("
|
|
1163
|
+
f"logical_name={self.logical_name!r}, revision={self.revision}, "
|
|
1164
|
+
f"artifact_sha256={self.artifact_sha256!r}, payload=<redacted>)"
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
@dataclass(frozen=True, slots=True, eq=False, repr=False)
|
|
1169
|
+
class _SealedWorkerActivation:
|
|
1170
|
+
"""Private capability proving one exact pending candidate was admitted."""
|
|
1171
|
+
|
|
1172
|
+
candidate: WorkerUniverseCandidate
|
|
1173
|
+
artifact_views: tuple[_ValidatedWorkerModuleArtifact, ...]
|
|
1174
|
+
stage_entries: tuple[_SealedWorkerStageEntry, ...]
|
|
1175
|
+
diagnostics: tuple[WorkerDiagnosticArtifact, ...]
|
|
1176
|
+
debug_view: WorkerGenerationDebugView | None = None
|
|
1177
|
+
|
|
1178
|
+
def __repr__(self) -> str:
|
|
1179
|
+
return (
|
|
1180
|
+
"_SealedWorkerActivation("
|
|
1181
|
+
f"generation={self.candidate.handle.generation}, "
|
|
1182
|
+
f"modules={len(self.artifact_views)}, payload=<redacted>)"
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _seal_worker_activation(
|
|
1187
|
+
candidate: WorkerUniverseCandidate,
|
|
1188
|
+
artifact_views: tuple[_ValidatedWorkerModuleArtifact, ...],
|
|
1189
|
+
) -> _SealedWorkerActivation:
|
|
1190
|
+
"""Bind the already-admitted manifest views to their exact candidate."""
|
|
1191
|
+
if (
|
|
1192
|
+
not isinstance(candidate, WorkerUniverseCandidate)
|
|
1193
|
+
or type(artifact_views) is not tuple
|
|
1194
|
+
or len(artifact_views) != len(candidate.manifest.modules)
|
|
1195
|
+
or len(artifact_views) != len(candidate.artifacts)
|
|
1196
|
+
):
|
|
1197
|
+
raise ProtocolError("Worker sealed activation is invalid")
|
|
1198
|
+
entries: list[_SealedWorkerStageEntry] = []
|
|
1199
|
+
diagnostics: list[WorkerDiagnosticArtifact] = []
|
|
1200
|
+
for view, artifact, module in zip(
|
|
1201
|
+
artifact_views,
|
|
1202
|
+
candidate.artifacts,
|
|
1203
|
+
candidate.manifest.modules,
|
|
1204
|
+
strict=True,
|
|
1205
|
+
):
|
|
1206
|
+
if (
|
|
1207
|
+
not isinstance(view, _ValidatedWorkerModuleArtifact)
|
|
1208
|
+
or view.descriptor is not artifact
|
|
1209
|
+
or not isinstance(view.snapshot, _AdmittedWorkerSnapshot)
|
|
1210
|
+
or artifact.logical_name != module.logical_name
|
|
1211
|
+
or artifact.revision != module.revision
|
|
1212
|
+
or artifact.artifact_sha256 != module.artifact_sha256
|
|
1213
|
+
or view.snapshot.source_sha256 != artifact.source_sha256
|
|
1214
|
+
or view.snapshot.artifact_sha256 != artifact.artifact_sha256
|
|
1215
|
+
or view.snapshot.source_map_sha256 != artifact.source_map_sha256
|
|
1216
|
+
or view.snapshot.mapped_source.source_map_sha256
|
|
1217
|
+
!= artifact.source_map_sha256
|
|
1218
|
+
):
|
|
1219
|
+
raise ProtocolError("Worker sealed activation is invalid")
|
|
1220
|
+
artifact_bytes = view.snapshot.artifact_bytes
|
|
1221
|
+
if type(artifact_bytes) is not bytes:
|
|
1222
|
+
raise ProtocolError("Worker sealed activation is invalid")
|
|
1223
|
+
entries.append(
|
|
1224
|
+
_SealedWorkerStageEntry(
|
|
1225
|
+
artifact,
|
|
1226
|
+
module.logical_name,
|
|
1227
|
+
module.revision,
|
|
1228
|
+
module.artifact_sha256,
|
|
1229
|
+
module.registration_name,
|
|
1230
|
+
artifact_bytes,
|
|
1231
|
+
view.snapshot,
|
|
1232
|
+
)
|
|
1233
|
+
)
|
|
1234
|
+
diagnostics.append(
|
|
1235
|
+
_worker_artifact_diagnostic_source_from_snapshot(
|
|
1236
|
+
artifact.worker_artifact,
|
|
1237
|
+
view.snapshot,
|
|
1238
|
+
logical_name=module.logical_name,
|
|
1239
|
+
revision=module.revision,
|
|
1240
|
+
registration_name=module.registration_name,
|
|
1241
|
+
manifest_sha256=candidate.manifest.sha256,
|
|
1242
|
+
artifact_sha256=module.artifact_sha256,
|
|
1243
|
+
source_map_sha256=artifact.source_map_sha256,
|
|
1244
|
+
)
|
|
1245
|
+
)
|
|
1246
|
+
return _SealedWorkerActivation(
|
|
1247
|
+
candidate,
|
|
1248
|
+
artifact_views,
|
|
1249
|
+
tuple(entries),
|
|
1250
|
+
tuple(diagnostics),
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
1255
|
+
class WorkerPreparedRootReceipt:
|
|
1256
|
+
transaction_id: UUID
|
|
1257
|
+
generation: int
|
|
1258
|
+
manifest_sha256: str
|
|
1259
|
+
candidate_root_key: str
|
|
1260
|
+
previous_root_key: str
|
|
1261
|
+
generation_create_wire_probe_ms: int
|
|
1262
|
+
|
|
1263
|
+
def __post_init__(self) -> None:
|
|
1264
|
+
if (
|
|
1265
|
+
type(self.transaction_id) is not UUID
|
|
1266
|
+
or type(self.generation) is not int
|
|
1267
|
+
or self.generation <= 0
|
|
1268
|
+
or not _sha256_value(self.manifest_sha256)
|
|
1269
|
+
or not _safe_identity(self.candidate_root_key)
|
|
1270
|
+
or (
|
|
1271
|
+
self.previous_root_key != ""
|
|
1272
|
+
and not _safe_identity(self.previous_root_key)
|
|
1273
|
+
)
|
|
1274
|
+
or self.previous_root_key == self.candidate_root_key
|
|
1275
|
+
or type(self.generation_create_wire_probe_ms) is not int
|
|
1276
|
+
or self.generation_create_wire_probe_ms < 0
|
|
1277
|
+
):
|
|
1278
|
+
raise ValueError("worker prepared root receipt is invalid")
|
|
1279
|
+
|
|
1280
|
+
def __repr__(self) -> str:
|
|
1281
|
+
return (
|
|
1282
|
+
"WorkerPreparedRootReceipt("
|
|
1283
|
+
f"transaction_id={self.transaction_id!r}, "
|
|
1284
|
+
f"generation={self.generation}, "
|
|
1285
|
+
f"manifest_sha256={self.manifest_sha256!r}, "
|
|
1286
|
+
"candidate_root_key=<redacted>, previous_root_key=<redacted>, "
|
|
1287
|
+
"generation_create_wire_probe_ms=<redacted>)"
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
1292
|
+
class WorkerPromotionReceipt:
|
|
1293
|
+
transaction_id: UUID
|
|
1294
|
+
generation: int
|
|
1295
|
+
manifest_sha256: str
|
|
1296
|
+
root_key: str
|
|
1297
|
+
previous_root_key: str
|
|
1298
|
+
acknowledged: bool
|
|
1299
|
+
generation_create_wire_probe_ms: int
|
|
1300
|
+
root_swap_ms: int
|
|
1301
|
+
|
|
1302
|
+
def __post_init__(self) -> None:
|
|
1303
|
+
if (
|
|
1304
|
+
type(self.transaction_id) is not UUID
|
|
1305
|
+
or type(self.generation) is not int
|
|
1306
|
+
or self.generation <= 0
|
|
1307
|
+
or not _sha256_value(self.manifest_sha256)
|
|
1308
|
+
or not _safe_identity(self.root_key)
|
|
1309
|
+
or (
|
|
1310
|
+
self.previous_root_key != ""
|
|
1311
|
+
and not _safe_identity(self.previous_root_key)
|
|
1312
|
+
)
|
|
1313
|
+
or self.previous_root_key == self.root_key
|
|
1314
|
+
or type(self.acknowledged) is not bool
|
|
1315
|
+
or type(self.generation_create_wire_probe_ms) is not int
|
|
1316
|
+
or self.generation_create_wire_probe_ms < 0
|
|
1317
|
+
or type(self.root_swap_ms) is not int
|
|
1318
|
+
or self.root_swap_ms < 0
|
|
1319
|
+
):
|
|
1320
|
+
raise ValueError("worker promotion receipt is invalid")
|
|
1321
|
+
|
|
1322
|
+
def __repr__(self) -> str:
|
|
1323
|
+
return (
|
|
1324
|
+
"WorkerPromotionReceipt("
|
|
1325
|
+
f"transaction_id={self.transaction_id!r}, "
|
|
1326
|
+
f"generation={self.generation}, "
|
|
1327
|
+
f"manifest_sha256={self.manifest_sha256!r}, "
|
|
1328
|
+
"root_key=<redacted>, previous_root_key=<redacted>, "
|
|
1329
|
+
f"acknowledged={self.acknowledged!r}, "
|
|
1330
|
+
"generation_create_wire_probe_ms=<redacted>, "
|
|
1331
|
+
"root_swap_ms=<redacted>)"
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
@dataclass(slots=True)
|
|
1336
|
+
class _GenerationRecord:
|
|
1337
|
+
handle: WorkerGenerationHandle
|
|
1338
|
+
manifest: WorkerUniverseManifest
|
|
1339
|
+
export_catalog: tuple[WorkerExport, ...]
|
|
1340
|
+
root_key: str
|
|
1341
|
+
active: bool
|
|
1342
|
+
explicitly_retained: bool
|
|
1343
|
+
descriptor_cache_identities: frozenset[tuple[str, str, int, str, str]]
|
|
1344
|
+
binary_keys: frozenset[WorkerModuleBinaryKey]
|
|
1345
|
+
debug_view: WorkerGenerationDebugView | None
|
|
1346
|
+
operation_pins: int = 0
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
@dataclass(slots=True)
|
|
1350
|
+
class _OperationLease:
|
|
1351
|
+
pin: OperationGenerationPin
|
|
1352
|
+
outcome_unknown: bool = False
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
GenerationFence = int | Callable[[], int]
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
@dataclass(frozen=True, slots=True)
|
|
1359
|
+
class _WorkerUniverseLiveInventory:
|
|
1360
|
+
manifest_sha256s: frozenset[str]
|
|
1361
|
+
artifact_identities: frozenset[tuple[str, int, str]]
|
|
1362
|
+
descriptor_cache_identities: frozenset[tuple[str, str, int, str, str]]
|
|
1363
|
+
binary_keys: frozenset[WorkerModuleBinaryKey]
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
1367
|
+
class WorkerLifecycleReleasePlan:
|
|
1368
|
+
owner: object
|
|
1369
|
+
expected_fence: tuple[int, int]
|
|
1370
|
+
release: WorkerGenerationHandle | OperationGenerationPin
|
|
1371
|
+
remaining_views: tuple[WorkerGenerationDebugView, ...]
|
|
1372
|
+
released_names: tuple[str, ...]
|
|
1373
|
+
next_registration_refcounts: tuple[tuple[str, int], ...]
|
|
1374
|
+
next_registration_artifacts: tuple[tuple[str, tuple[str, str]], ...]
|
|
1375
|
+
|
|
1376
|
+
def __repr__(self) -> str:
|
|
1377
|
+
return (
|
|
1378
|
+
"WorkerLifecycleReleasePlan(owner=<redacted>, "
|
|
1379
|
+
f"expected_fence={self.expected_fence!r}, "
|
|
1380
|
+
f"release={type(self.release).__name__}, "
|
|
1381
|
+
f"remaining_views={len(self.remaining_views)}, "
|
|
1382
|
+
f"released_names={len(self.released_names)})"
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
class WorkerUniverseRegistry:
|
|
1387
|
+
"""Atomic host registry for immutable Worker module generations."""
|
|
1388
|
+
|
|
1389
|
+
def __init__(
|
|
1390
|
+
self,
|
|
1391
|
+
*,
|
|
1392
|
+
runtime_generation: GenerationFence,
|
|
1393
|
+
context_generation: GenerationFence,
|
|
1394
|
+
) -> None:
|
|
1395
|
+
self._lock = RLock()
|
|
1396
|
+
self._runtime_generation_source = _fence_source(runtime_generation)
|
|
1397
|
+
self._context_generation_source = _fence_source(context_generation)
|
|
1398
|
+
self._runtime_generation = self._runtime_generation_source()
|
|
1399
|
+
self._context_generation = self._context_generation_source()
|
|
1400
|
+
_validate_positive_generation(self._runtime_generation)
|
|
1401
|
+
_validate_positive_generation(self._context_generation)
|
|
1402
|
+
self._state = WorkerUniverseState.EMPTY
|
|
1403
|
+
self._next_generation = 1
|
|
1404
|
+
self._pending: WorkerUniverseCandidate | None = None
|
|
1405
|
+
self._sealed_activations: dict[int, _SealedWorkerActivation] = {}
|
|
1406
|
+
self._active_generation: int | None = None
|
|
1407
|
+
self._handles: dict[int, WorkerGenerationHandle] = {}
|
|
1408
|
+
self._generations: dict[int, _GenerationRecord] = {}
|
|
1409
|
+
self._leases: dict[str, _OperationLease] = {}
|
|
1410
|
+
self._registration_refcounts: dict[str, int] = {}
|
|
1411
|
+
self._registration_artifacts: dict[str, tuple[str, str]] = {}
|
|
1412
|
+
self._quarantine_holds: set[str] = set()
|
|
1413
|
+
self._release_owner = object()
|
|
1414
|
+
|
|
1415
|
+
@property
|
|
1416
|
+
def state(self) -> WorkerUniverseState:
|
|
1417
|
+
with self._lock:
|
|
1418
|
+
return self._state
|
|
1419
|
+
|
|
1420
|
+
@property
|
|
1421
|
+
def active_handle(self) -> WorkerGenerationHandle | None:
|
|
1422
|
+
with self._lock:
|
|
1423
|
+
self._require_active_read()
|
|
1424
|
+
record = self._active_record()
|
|
1425
|
+
return None if record is None else record.handle
|
|
1426
|
+
|
|
1427
|
+
@property
|
|
1428
|
+
def active_manifest(self) -> WorkerUniverseManifest | None:
|
|
1429
|
+
with self._lock:
|
|
1430
|
+
self._require_active_read()
|
|
1431
|
+
record = self._active_record()
|
|
1432
|
+
return None if record is None else record.manifest
|
|
1433
|
+
|
|
1434
|
+
@property
|
|
1435
|
+
def active_root_key(self) -> str | None:
|
|
1436
|
+
with self._lock:
|
|
1437
|
+
self._require_active_read()
|
|
1438
|
+
record = self._active_record()
|
|
1439
|
+
return None if record is None else record.root_key
|
|
1440
|
+
|
|
1441
|
+
def _confirmed_live_inventory(self) -> _WorkerUniverseLiveInventory | None:
|
|
1442
|
+
"""Return exact cache roots only while host lifecycle state is certain."""
|
|
1443
|
+
with self._lock:
|
|
1444
|
+
if self._state not in (
|
|
1445
|
+
WorkerUniverseState.EMPTY,
|
|
1446
|
+
WorkerUniverseState.READY,
|
|
1447
|
+
):
|
|
1448
|
+
return None
|
|
1449
|
+
try:
|
|
1450
|
+
self._require_current_fence()
|
|
1451
|
+
except ProtocolError:
|
|
1452
|
+
return None
|
|
1453
|
+
manifests = tuple(
|
|
1454
|
+
record.manifest
|
|
1455
|
+
for record in self._generations.values()
|
|
1456
|
+
if (
|
|
1457
|
+
record.active
|
|
1458
|
+
or record.explicitly_retained
|
|
1459
|
+
or record.operation_pins > 0
|
|
1460
|
+
)
|
|
1461
|
+
)
|
|
1462
|
+
return _WorkerUniverseLiveInventory(
|
|
1463
|
+
frozenset(manifest.sha256 for manifest in manifests),
|
|
1464
|
+
frozenset(
|
|
1465
|
+
(
|
|
1466
|
+
module.logical_name.casefold(),
|
|
1467
|
+
module.revision,
|
|
1468
|
+
module.artifact_sha256,
|
|
1469
|
+
)
|
|
1470
|
+
for manifest in manifests
|
|
1471
|
+
for module in manifest.modules
|
|
1472
|
+
),
|
|
1473
|
+
frozenset(
|
|
1474
|
+
identity
|
|
1475
|
+
for record in self._generations.values()
|
|
1476
|
+
if (
|
|
1477
|
+
record.active
|
|
1478
|
+
or record.explicitly_retained
|
|
1479
|
+
or record.operation_pins > 0
|
|
1480
|
+
)
|
|
1481
|
+
for identity in record.descriptor_cache_identities
|
|
1482
|
+
),
|
|
1483
|
+
frozenset(
|
|
1484
|
+
binary_key
|
|
1485
|
+
for record in self._generations.values()
|
|
1486
|
+
if (
|
|
1487
|
+
record.active
|
|
1488
|
+
or record.explicitly_retained
|
|
1489
|
+
or record.operation_pins > 0
|
|
1490
|
+
)
|
|
1491
|
+
for binary_key in record.binary_keys
|
|
1492
|
+
),
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
def preview_handle(
|
|
1496
|
+
self,
|
|
1497
|
+
artifacts: tuple[WorkerModuleArtifact, ...],
|
|
1498
|
+
*,
|
|
1499
|
+
export_catalog: tuple[WorkerExport, ...] | None = None,
|
|
1500
|
+
) -> WorkerGenerationHandle:
|
|
1501
|
+
"""Preview the next exact identity without reserving or staging it.
|
|
1502
|
+
|
|
1503
|
+
A later preparation (including an aborted one) can invalidate this
|
|
1504
|
+
preview. Callers must fence the actual candidate before target mutation.
|
|
1505
|
+
"""
|
|
1506
|
+
with self._lock:
|
|
1507
|
+
self._require_available("preview")
|
|
1508
|
+
if self._state is WorkerUniverseState.PREPARING:
|
|
1509
|
+
raise ProtocolError("Worker universe already has a pending candidate")
|
|
1510
|
+
self._require_current_fence()
|
|
1511
|
+
_, manifest = _build_worker_manifest(self._next_generation, artifacts)
|
|
1512
|
+
catalog = manifest.exports if export_catalog is None else export_catalog
|
|
1513
|
+
if not _effective_export_catalog_matches(manifest, catalog):
|
|
1514
|
+
raise ProtocolError("Worker generation export catalog does not match its manifest")
|
|
1515
|
+
_require_registration_ownership(manifest, self._registration_artifacts)
|
|
1516
|
+
return WorkerGenerationHandle(
|
|
1517
|
+
self._runtime_generation, self._context_generation,
|
|
1518
|
+
self._next_generation, manifest.sha256,
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
def prepare(
|
|
1522
|
+
self,
|
|
1523
|
+
artifacts: tuple[WorkerModuleArtifact, ...],
|
|
1524
|
+
*,
|
|
1525
|
+
export_catalog: tuple[WorkerExport, ...] | None = None,
|
|
1526
|
+
) -> WorkerUniverseCandidate:
|
|
1527
|
+
with self._lock:
|
|
1528
|
+
self._require_available("prepare")
|
|
1529
|
+
if self._state is WorkerUniverseState.PREPARING:
|
|
1530
|
+
raise ProtocolError("Worker universe already has a pending candidate")
|
|
1531
|
+
self._require_current_fence()
|
|
1532
|
+
generation = self._next_generation
|
|
1533
|
+
ordered_views, manifest = _build_worker_manifest(generation, artifacts)
|
|
1534
|
+
ordered = tuple(view.descriptor for view in ordered_views)
|
|
1535
|
+
effective_catalog = (
|
|
1536
|
+
manifest.exports if export_catalog is None else export_catalog
|
|
1537
|
+
)
|
|
1538
|
+
if not _effective_export_catalog_matches(manifest, effective_catalog):
|
|
1539
|
+
raise ProtocolError(
|
|
1540
|
+
"Worker generation export catalog does not match its manifest"
|
|
1541
|
+
)
|
|
1542
|
+
_require_registration_ownership(
|
|
1543
|
+
manifest,
|
|
1544
|
+
self._registration_artifacts,
|
|
1545
|
+
)
|
|
1546
|
+
handle = WorkerGenerationHandle(
|
|
1547
|
+
self._runtime_generation,
|
|
1548
|
+
self._context_generation,
|
|
1549
|
+
generation,
|
|
1550
|
+
manifest.sha256,
|
|
1551
|
+
)
|
|
1552
|
+
new_artifacts = tuple(
|
|
1553
|
+
artifact
|
|
1554
|
+
for artifact, module in zip(ordered, manifest.modules, strict=True)
|
|
1555
|
+
if module.registration_name not in self._registration_refcounts
|
|
1556
|
+
)
|
|
1557
|
+
previous = self._active_record()
|
|
1558
|
+
candidate = WorkerUniverseCandidate(
|
|
1559
|
+
handle,
|
|
1560
|
+
manifest,
|
|
1561
|
+
effective_catalog,
|
|
1562
|
+
ordered,
|
|
1563
|
+
new_artifacts,
|
|
1564
|
+
None if previous is None else previous.handle,
|
|
1565
|
+
)
|
|
1566
|
+
activation = _seal_worker_activation(candidate, ordered_views)
|
|
1567
|
+
self._next_generation += 1
|
|
1568
|
+
self._handles[generation] = handle
|
|
1569
|
+
self._pending = candidate
|
|
1570
|
+
self._sealed_activations[generation] = activation
|
|
1571
|
+
self._state = WorkerUniverseState.PREPARING
|
|
1572
|
+
return candidate
|
|
1573
|
+
|
|
1574
|
+
def discard(self, candidate: WorkerUniverseCandidate) -> None:
|
|
1575
|
+
with self._lock:
|
|
1576
|
+
self._require_available("discard")
|
|
1577
|
+
self._require_pending(candidate)
|
|
1578
|
+
try:
|
|
1579
|
+
self._require_current_fence()
|
|
1580
|
+
except ProtocolError:
|
|
1581
|
+
self._quarantine_candidate(candidate)
|
|
1582
|
+
self._pending = None
|
|
1583
|
+
self._state = WorkerUniverseState.BROKEN
|
|
1584
|
+
raise
|
|
1585
|
+
self._handles.pop(candidate.handle.generation, None)
|
|
1586
|
+
self._sealed_activations.pop(candidate.handle.generation, None)
|
|
1587
|
+
self._pending = None
|
|
1588
|
+
self._state = (
|
|
1589
|
+
WorkerUniverseState.READY
|
|
1590
|
+
if self._active_generation is not None
|
|
1591
|
+
else WorkerUniverseState.EMPTY
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
def _discard_exact_pending_without_fence(
|
|
1595
|
+
self,
|
|
1596
|
+
candidate: WorkerUniverseCandidate,
|
|
1597
|
+
) -> None:
|
|
1598
|
+
"""Undo an internally returned exact candidate before target access.
|
|
1599
|
+
|
|
1600
|
+
Server preparation alone may use this after its post-prepare fence
|
|
1601
|
+
check fails. Caller-supplied promotion still uses the public,
|
|
1602
|
+
fence-authenticated lifecycle paths.
|
|
1603
|
+
"""
|
|
1604
|
+
|
|
1605
|
+
with self._lock:
|
|
1606
|
+
self._require_available("internal candidate cleanup")
|
|
1607
|
+
self._require_pending(candidate)
|
|
1608
|
+
activation = self._sealed_activations.get(
|
|
1609
|
+
candidate.handle.generation
|
|
1610
|
+
)
|
|
1611
|
+
if activation is None or activation.candidate is not candidate:
|
|
1612
|
+
raise ProtocolError(
|
|
1613
|
+
"Worker internal candidate cleanup is uncertain"
|
|
1614
|
+
)
|
|
1615
|
+
self._handles.pop(candidate.handle.generation, None)
|
|
1616
|
+
self._sealed_activations.pop(candidate.handle.generation, None)
|
|
1617
|
+
self._pending = None
|
|
1618
|
+
self._state = (
|
|
1619
|
+
WorkerUniverseState.READY
|
|
1620
|
+
if self._active_generation is not None
|
|
1621
|
+
else WorkerUniverseState.EMPTY
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
def confirm(
|
|
1625
|
+
self,
|
|
1626
|
+
candidate: WorkerUniverseCandidate,
|
|
1627
|
+
receipt: WorkerPromotionReceipt,
|
|
1628
|
+
) -> WorkerGenerationConfirmation:
|
|
1629
|
+
with self._lock:
|
|
1630
|
+
self._require_available("confirm")
|
|
1631
|
+
self._require_current_fence()
|
|
1632
|
+
self._require_pending(candidate)
|
|
1633
|
+
current = self._active_record()
|
|
1634
|
+
expected_previous_root = "" if current is None else current.root_key
|
|
1635
|
+
if (
|
|
1636
|
+
not isinstance(receipt, WorkerPromotionReceipt)
|
|
1637
|
+
or not receipt.acknowledged
|
|
1638
|
+
or receipt.generation != candidate.handle.generation
|
|
1639
|
+
or receipt.manifest_sha256 != candidate.manifest.sha256
|
|
1640
|
+
or receipt.root_key != _generation_root_key(candidate.handle.generation)
|
|
1641
|
+
or receipt.previous_root_key != expected_previous_root
|
|
1642
|
+
):
|
|
1643
|
+
raise ProtocolError("Worker promotion receipt is stale or unacknowledged")
|
|
1644
|
+
|
|
1645
|
+
registration_refcounts = dict(self._registration_refcounts)
|
|
1646
|
+
registration_artifacts = dict(self._registration_artifacts)
|
|
1647
|
+
old_record = current
|
|
1648
|
+
if old_record is not None and not old_record.explicitly_retained:
|
|
1649
|
+
_adjust_manifest_registrations(
|
|
1650
|
+
registration_refcounts,
|
|
1651
|
+
old_record.manifest,
|
|
1652
|
+
-1,
|
|
1653
|
+
)
|
|
1654
|
+
_adjust_manifest_registrations(
|
|
1655
|
+
registration_refcounts,
|
|
1656
|
+
candidate.manifest,
|
|
1657
|
+
1,
|
|
1658
|
+
)
|
|
1659
|
+
_require_registration_ownership(
|
|
1660
|
+
candidate.manifest,
|
|
1661
|
+
registration_artifacts,
|
|
1662
|
+
)
|
|
1663
|
+
for module in candidate.manifest.modules:
|
|
1664
|
+
registration_artifacts[module.registration_name] = (
|
|
1665
|
+
module.logical_name.casefold(),
|
|
1666
|
+
module.artifact_sha256,
|
|
1667
|
+
)
|
|
1668
|
+
released = _remove_zero_registration_refcounts(
|
|
1669
|
+
registration_refcounts,
|
|
1670
|
+
registration_artifacts,
|
|
1671
|
+
self._quarantine_holds,
|
|
1672
|
+
)
|
|
1673
|
+
confirmation = WorkerGenerationConfirmation(
|
|
1674
|
+
candidate.handle,
|
|
1675
|
+
released,
|
|
1676
|
+
)
|
|
1677
|
+
record = _GenerationRecord(
|
|
1678
|
+
candidate.handle,
|
|
1679
|
+
candidate.manifest,
|
|
1680
|
+
candidate.export_catalog,
|
|
1681
|
+
receipt.root_key,
|
|
1682
|
+
active=True,
|
|
1683
|
+
explicitly_retained=True,
|
|
1684
|
+
descriptor_cache_identities=frozenset(
|
|
1685
|
+
identity
|
|
1686
|
+
for artifact in candidate.artifacts
|
|
1687
|
+
if (
|
|
1688
|
+
identity := _worker_module_descriptor_cache_identity(
|
|
1689
|
+
artifact
|
|
1690
|
+
)
|
|
1691
|
+
)
|
|
1692
|
+
is not None
|
|
1693
|
+
),
|
|
1694
|
+
binary_keys=frozenset(
|
|
1695
|
+
artifact.binary_key for artifact in candidate.artifacts
|
|
1696
|
+
),
|
|
1697
|
+
debug_view=self._sealed_activations[
|
|
1698
|
+
candidate.handle.generation
|
|
1699
|
+
].debug_view,
|
|
1700
|
+
)
|
|
1701
|
+
|
|
1702
|
+
if old_record is not None:
|
|
1703
|
+
old_record.active = False
|
|
1704
|
+
self._generations[candidate.handle.generation] = record
|
|
1705
|
+
self._active_generation = candidate.handle.generation
|
|
1706
|
+
self._registration_refcounts = registration_refcounts
|
|
1707
|
+
self._registration_artifacts = registration_artifacts
|
|
1708
|
+
self._sealed_activations.pop(candidate.handle.generation, None)
|
|
1709
|
+
self._pending = None
|
|
1710
|
+
self._state = WorkerUniverseState.READY
|
|
1711
|
+
self._collect_unretained_generations()
|
|
1712
|
+
return confirmation
|
|
1713
|
+
|
|
1714
|
+
def pin_active(self) -> OperationGenerationPin:
|
|
1715
|
+
with self._lock:
|
|
1716
|
+
self._require_available("pin")
|
|
1717
|
+
self._require_current_fence()
|
|
1718
|
+
record = self._active_record()
|
|
1719
|
+
if record is None:
|
|
1720
|
+
raise ProtocolError("Worker universe has no active generation")
|
|
1721
|
+
lease_id = _new_lease_id(self._leases)
|
|
1722
|
+
pin = OperationGenerationPin(
|
|
1723
|
+
record.handle,
|
|
1724
|
+
record.manifest,
|
|
1725
|
+
record.root_key,
|
|
1726
|
+
record.export_catalog,
|
|
1727
|
+
lease_id,
|
|
1728
|
+
)
|
|
1729
|
+
registration_refcounts = dict(self._registration_refcounts)
|
|
1730
|
+
_adjust_manifest_registrations(
|
|
1731
|
+
registration_refcounts,
|
|
1732
|
+
record.manifest,
|
|
1733
|
+
1,
|
|
1734
|
+
)
|
|
1735
|
+
record.operation_pins += 1
|
|
1736
|
+
self._registration_refcounts = registration_refcounts
|
|
1737
|
+
self._leases[lease_id] = _OperationLease(pin)
|
|
1738
|
+
return pin
|
|
1739
|
+
|
|
1740
|
+
def release_generation(
|
|
1741
|
+
self,
|
|
1742
|
+
handle: WorkerGenerationHandle,
|
|
1743
|
+
) -> tuple[str, ...]:
|
|
1744
|
+
return self._commit_release(self._preview_release(handle))
|
|
1745
|
+
|
|
1746
|
+
def release_pin(self, pin: OperationGenerationPin) -> tuple[str, ...]:
|
|
1747
|
+
return self._commit_release(self._preview_release(pin))
|
|
1748
|
+
|
|
1749
|
+
def _preview_release(
|
|
1750
|
+
self,
|
|
1751
|
+
release: WorkerGenerationHandle | OperationGenerationPin,
|
|
1752
|
+
) -> WorkerLifecycleReleasePlan:
|
|
1753
|
+
with self._lock:
|
|
1754
|
+
self._require_available("preview release")
|
|
1755
|
+
self._require_current_fence()
|
|
1756
|
+
refcounts = dict(self._registration_refcounts)
|
|
1757
|
+
artifacts = dict(self._registration_artifacts)
|
|
1758
|
+
if type(release) is WorkerGenerationHandle:
|
|
1759
|
+
record = self._require_generation(release)
|
|
1760
|
+
if not record.explicitly_retained:
|
|
1761
|
+
raise ProtocolError("Worker generation handle is stale or released")
|
|
1762
|
+
if not record.active:
|
|
1763
|
+
_adjust_manifest_registrations(refcounts, record.manifest, -1)
|
|
1764
|
+
elif type(release) is OperationGenerationPin:
|
|
1765
|
+
lease = self._require_lease(release)
|
|
1766
|
+
if lease.outcome_unknown:
|
|
1767
|
+
raise ProtocolError(
|
|
1768
|
+
"Outcome-unknown Worker pin is retained until teardown"
|
|
1769
|
+
)
|
|
1770
|
+
record = self._require_generation(release.handle)
|
|
1771
|
+
if record.operation_pins <= 0:
|
|
1772
|
+
raise ProtocolError("Worker generation pin refcount is invalid")
|
|
1773
|
+
_adjust_manifest_registrations(refcounts, record.manifest, -1)
|
|
1774
|
+
else:
|
|
1775
|
+
raise TypeError("Worker lifecycle release capability is required")
|
|
1776
|
+
released = _remove_zero_registration_refcounts(
|
|
1777
|
+
refcounts,
|
|
1778
|
+
artifacts,
|
|
1779
|
+
self._quarantine_holds,
|
|
1780
|
+
)
|
|
1781
|
+
remaining = tuple(
|
|
1782
|
+
item.debug_view
|
|
1783
|
+
for _generation, item in sorted(self._generations.items())
|
|
1784
|
+
if item.debug_view is not None
|
|
1785
|
+
and self._record_live_after_release(item, record, release)
|
|
1786
|
+
)
|
|
1787
|
+
return WorkerLifecycleReleasePlan(
|
|
1788
|
+
self._release_owner,
|
|
1789
|
+
(self._runtime_generation, self._context_generation),
|
|
1790
|
+
release,
|
|
1791
|
+
remaining,
|
|
1792
|
+
released,
|
|
1793
|
+
tuple(sorted(refcounts.items(), key=lambda item: item[0].casefold())),
|
|
1794
|
+
tuple(sorted(artifacts.items(), key=lambda item: item[0].casefold())),
|
|
1795
|
+
)
|
|
1796
|
+
|
|
1797
|
+
def _commit_release(
|
|
1798
|
+
self,
|
|
1799
|
+
plan: WorkerLifecycleReleasePlan,
|
|
1800
|
+
) -> tuple[str, ...]:
|
|
1801
|
+
if type(plan) is not WorkerLifecycleReleasePlan:
|
|
1802
|
+
raise TypeError("Worker lifecycle release plan is required")
|
|
1803
|
+
with self._lock:
|
|
1804
|
+
self._require_available("commit release")
|
|
1805
|
+
self._require_current_fence()
|
|
1806
|
+
if (
|
|
1807
|
+
plan.owner is not self._release_owner
|
|
1808
|
+
or plan.expected_fence
|
|
1809
|
+
!= (self._runtime_generation, self._context_generation)
|
|
1810
|
+
):
|
|
1811
|
+
raise ProtocolError("Worker lifecycle release plan is stale")
|
|
1812
|
+
current = self._preview_release(plan.release)
|
|
1813
|
+
if (
|
|
1814
|
+
current.remaining_views != plan.remaining_views
|
|
1815
|
+
or current.released_names != plan.released_names
|
|
1816
|
+
or current.next_registration_refcounts
|
|
1817
|
+
!= plan.next_registration_refcounts
|
|
1818
|
+
or current.next_registration_artifacts
|
|
1819
|
+
!= plan.next_registration_artifacts
|
|
1820
|
+
):
|
|
1821
|
+
raise ProtocolError("Worker lifecycle release plan is stale")
|
|
1822
|
+
release = plan.release
|
|
1823
|
+
if type(release) is WorkerGenerationHandle:
|
|
1824
|
+
record = self._require_generation(release)
|
|
1825
|
+
record.explicitly_retained = False
|
|
1826
|
+
else:
|
|
1827
|
+
lease = self._require_lease(release)
|
|
1828
|
+
record = self._require_generation(release.handle)
|
|
1829
|
+
del self._leases[release.lease_id]
|
|
1830
|
+
record.operation_pins -= 1
|
|
1831
|
+
self._registration_refcounts = dict(plan.next_registration_refcounts)
|
|
1832
|
+
self._registration_artifacts = dict(plan.next_registration_artifacts)
|
|
1833
|
+
self._collect_unretained_generations()
|
|
1834
|
+
return plan.released_names
|
|
1835
|
+
|
|
1836
|
+
@staticmethod
|
|
1837
|
+
def _record_live_after_release(
|
|
1838
|
+
item: _GenerationRecord,
|
|
1839
|
+
released_record: _GenerationRecord,
|
|
1840
|
+
release: WorkerGenerationHandle | OperationGenerationPin,
|
|
1841
|
+
) -> bool:
|
|
1842
|
+
explicitly_retained = item.explicitly_retained
|
|
1843
|
+
operation_pins = item.operation_pins
|
|
1844
|
+
if item is released_record:
|
|
1845
|
+
if type(release) is WorkerGenerationHandle:
|
|
1846
|
+
explicitly_retained = False
|
|
1847
|
+
else:
|
|
1848
|
+
operation_pins -= 1
|
|
1849
|
+
return item.active or explicitly_retained or operation_pins > 0
|
|
1850
|
+
|
|
1851
|
+
def retain_outcome_unknown(self, pin: OperationGenerationPin) -> None:
|
|
1852
|
+
with self._lock:
|
|
1853
|
+
self._require_available("retain outcome-unknown pin")
|
|
1854
|
+
lease = self._require_lease(pin)
|
|
1855
|
+
lease.outcome_unknown = True
|
|
1856
|
+
if self._pending is not None:
|
|
1857
|
+
self._quarantine_candidate(self._pending)
|
|
1858
|
+
self._state = WorkerUniverseState.BROKEN
|
|
1859
|
+
self._pending = None
|
|
1860
|
+
self._require_current_fence()
|
|
1861
|
+
|
|
1862
|
+
def mark_broken(
|
|
1863
|
+
self,
|
|
1864
|
+
candidate: WorkerUniverseCandidate | None = None,
|
|
1865
|
+
) -> None:
|
|
1866
|
+
with self._lock:
|
|
1867
|
+
self._require_available("mark broken")
|
|
1868
|
+
if candidate is not None:
|
|
1869
|
+
self._require_pending(candidate)
|
|
1870
|
+
elif self._pending is not None:
|
|
1871
|
+
candidate = self._pending
|
|
1872
|
+
if candidate is not None:
|
|
1873
|
+
self._quarantine_candidate(candidate)
|
|
1874
|
+
self._pending = None
|
|
1875
|
+
self._state = WorkerUniverseState.BROKEN
|
|
1876
|
+
|
|
1877
|
+
def registration_refcount(self, name: str) -> int:
|
|
1878
|
+
if not isinstance(name, str):
|
|
1879
|
+
raise TypeError("worker registration name must be a string")
|
|
1880
|
+
with self._lock:
|
|
1881
|
+
return self._registration_refcounts.get(name, 0) + (
|
|
1882
|
+
1 if name in self._quarantine_holds else 0
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
def teardown(self) -> tuple[str, ...]:
|
|
1886
|
+
with self._lock:
|
|
1887
|
+
if self._pending is not None:
|
|
1888
|
+
self._quarantine_candidate(self._pending)
|
|
1889
|
+
registrations = tuple(
|
|
1890
|
+
sorted(
|
|
1891
|
+
set(self._registration_refcounts) | self._quarantine_holds,
|
|
1892
|
+
key=str.casefold,
|
|
1893
|
+
)
|
|
1894
|
+
)
|
|
1895
|
+
self._pending = None
|
|
1896
|
+
self._active_generation = None
|
|
1897
|
+
self._sealed_activations.clear()
|
|
1898
|
+
self._handles.clear()
|
|
1899
|
+
self._generations.clear()
|
|
1900
|
+
self._leases.clear()
|
|
1901
|
+
self._registration_refcounts.clear()
|
|
1902
|
+
self._registration_artifacts.clear()
|
|
1903
|
+
self._quarantine_holds.clear()
|
|
1904
|
+
self._state = WorkerUniverseState.CLOSED
|
|
1905
|
+
return registrations
|
|
1906
|
+
|
|
1907
|
+
close = teardown
|
|
1908
|
+
|
|
1909
|
+
def _active_record(self) -> _GenerationRecord | None:
|
|
1910
|
+
if self._active_generation is None:
|
|
1911
|
+
return None
|
|
1912
|
+
return self._generations.get(self._active_generation)
|
|
1913
|
+
|
|
1914
|
+
def _require_available(self, operation: str) -> None:
|
|
1915
|
+
if self._state in (WorkerUniverseState.BROKEN, WorkerUniverseState.CLOSED):
|
|
1916
|
+
raise ProtocolError(
|
|
1917
|
+
f"Worker universe {operation} is unavailable in {self._state.value} state"
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
def _require_current_fence(self) -> None:
|
|
1921
|
+
if (
|
|
1922
|
+
self._runtime_generation_source() != self._runtime_generation
|
|
1923
|
+
or self._context_generation_source() != self._context_generation
|
|
1924
|
+
):
|
|
1925
|
+
raise ProtocolError("Worker universe runtime/context fence is stale")
|
|
1926
|
+
|
|
1927
|
+
def _require_active_read(self) -> None:
|
|
1928
|
+
if self._state is WorkerUniverseState.BROKEN:
|
|
1929
|
+
raise ProtocolError("Worker universe active generation is unavailable in broken state")
|
|
1930
|
+
if self._state is not WorkerUniverseState.CLOSED:
|
|
1931
|
+
self._require_current_fence()
|
|
1932
|
+
|
|
1933
|
+
def _require_pending(self, candidate: WorkerUniverseCandidate) -> None:
|
|
1934
|
+
if (
|
|
1935
|
+
not isinstance(candidate, WorkerUniverseCandidate)
|
|
1936
|
+
or self._state is not WorkerUniverseState.PREPARING
|
|
1937
|
+
or self._pending is not candidate
|
|
1938
|
+
or self._handles.get(candidate.handle.generation) is not candidate.handle
|
|
1939
|
+
):
|
|
1940
|
+
raise ProtocolError("Worker universe candidate is stale or forged")
|
|
1941
|
+
|
|
1942
|
+
def _require_sealed_activation(
|
|
1943
|
+
self,
|
|
1944
|
+
candidate: WorkerUniverseCandidate,
|
|
1945
|
+
) -> _SealedWorkerActivation:
|
|
1946
|
+
"""Return the O(1), exact-candidate capability for pending staging."""
|
|
1947
|
+
with self._lock:
|
|
1948
|
+
self._require_available("sealed activation")
|
|
1949
|
+
self._require_current_fence()
|
|
1950
|
+
self._require_pending(candidate)
|
|
1951
|
+
activation = self._sealed_activations.get(candidate.handle.generation)
|
|
1952
|
+
if activation is None or activation.candidate is not candidate:
|
|
1953
|
+
raise ProtocolError("Worker sealed activation is stale or forged")
|
|
1954
|
+
return activation
|
|
1955
|
+
|
|
1956
|
+
def _candidate_diagnostics(
|
|
1957
|
+
self,
|
|
1958
|
+
candidate: WorkerUniverseCandidate,
|
|
1959
|
+
) -> tuple[WorkerDiagnosticArtifact, ...]:
|
|
1960
|
+
"""Return the exact diagnostics sealed for one pending candidate."""
|
|
1961
|
+
with self._lock:
|
|
1962
|
+
return self._require_sealed_activation(candidate).diagnostics
|
|
1963
|
+
|
|
1964
|
+
def _bind_candidate_debug_view(
|
|
1965
|
+
self,
|
|
1966
|
+
candidate: WorkerUniverseCandidate,
|
|
1967
|
+
debug_view: WorkerGenerationDebugView,
|
|
1968
|
+
) -> None:
|
|
1969
|
+
with self._lock:
|
|
1970
|
+
activation = self._require_sealed_activation(candidate)
|
|
1971
|
+
if (
|
|
1972
|
+
type(debug_view) is not WorkerGenerationDebugView
|
|
1973
|
+
or debug_view.handle is not candidate.handle
|
|
1974
|
+
or debug_view.manifest is not candidate.manifest
|
|
1975
|
+
or activation.debug_view is not None
|
|
1976
|
+
):
|
|
1977
|
+
raise ProtocolError("Worker candidate debug view is invalid")
|
|
1978
|
+
self._sealed_activations[candidate.handle.generation] = (
|
|
1979
|
+
_SealedWorkerActivation(
|
|
1980
|
+
activation.candidate,
|
|
1981
|
+
activation.artifact_views,
|
|
1982
|
+
activation.stage_entries,
|
|
1983
|
+
activation.diagnostics,
|
|
1984
|
+
debug_view,
|
|
1985
|
+
)
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
def _candidate_debug_view(
|
|
1989
|
+
self,
|
|
1990
|
+
candidate: WorkerUniverseCandidate,
|
|
1991
|
+
) -> WorkerGenerationDebugView:
|
|
1992
|
+
with self._lock:
|
|
1993
|
+
view = self._require_sealed_activation(candidate).debug_view
|
|
1994
|
+
if view is None:
|
|
1995
|
+
raise ProtocolError("Worker candidate debug view is unavailable")
|
|
1996
|
+
return view
|
|
1997
|
+
|
|
1998
|
+
def _operation_debug_view(
|
|
1999
|
+
self,
|
|
2000
|
+
pin: OperationGenerationPin,
|
|
2001
|
+
) -> WorkerGenerationDebugView:
|
|
2002
|
+
with self._lock:
|
|
2003
|
+
self._require_current_fence()
|
|
2004
|
+
lease = self._require_lease(pin)
|
|
2005
|
+
record = self._require_generation(lease.pin.handle)
|
|
2006
|
+
if record.debug_view is None:
|
|
2007
|
+
raise ProtocolError("Worker generation debug view is unavailable")
|
|
2008
|
+
return record.debug_view
|
|
2009
|
+
|
|
2010
|
+
def _retained_debug_views(self) -> tuple[WorkerGenerationDebugView, ...]:
|
|
2011
|
+
with self._lock:
|
|
2012
|
+
if self._state is WorkerUniverseState.CLOSED:
|
|
2013
|
+
raise ProtocolError("Worker generation debug views are unavailable")
|
|
2014
|
+
self._require_current_fence()
|
|
2015
|
+
return tuple(
|
|
2016
|
+
record.debug_view
|
|
2017
|
+
for _generation, record in sorted(self._generations.items())
|
|
2018
|
+
if record.debug_view is not None
|
|
2019
|
+
and (
|
|
2020
|
+
record.active
|
|
2021
|
+
or record.explicitly_retained
|
|
2022
|
+
or record.operation_pins > 0
|
|
2023
|
+
)
|
|
2024
|
+
)
|
|
2025
|
+
|
|
2026
|
+
def _require_generation(
|
|
2027
|
+
self,
|
|
2028
|
+
handle: WorkerGenerationHandle,
|
|
2029
|
+
) -> _GenerationRecord:
|
|
2030
|
+
if (
|
|
2031
|
+
not isinstance(handle, WorkerGenerationHandle)
|
|
2032
|
+
or handle.runtime_generation != self._runtime_generation
|
|
2033
|
+
or handle.context_generation != self._context_generation
|
|
2034
|
+
or self._handles.get(handle.generation) is not handle
|
|
2035
|
+
):
|
|
2036
|
+
raise ProtocolError("Worker generation handle is stale or forged")
|
|
2037
|
+
record = self._generations.get(handle.generation)
|
|
2038
|
+
if (
|
|
2039
|
+
record is None
|
|
2040
|
+
or record.handle is not handle
|
|
2041
|
+
or handle.manifest_sha256 != record.manifest.sha256
|
|
2042
|
+
):
|
|
2043
|
+
raise ProtocolError("Worker generation handle is stale or released")
|
|
2044
|
+
return record
|
|
2045
|
+
|
|
2046
|
+
def _require_lease(self, pin: OperationGenerationPin) -> _OperationLease:
|
|
2047
|
+
if not isinstance(pin, OperationGenerationPin):
|
|
2048
|
+
raise ProtocolError("Worker generation pin is stale or forged")
|
|
2049
|
+
lease = self._leases.get(pin.lease_id)
|
|
2050
|
+
if lease is None or lease.pin is not pin:
|
|
2051
|
+
raise ProtocolError("Worker generation pin is stale or forged")
|
|
2052
|
+
return lease
|
|
2053
|
+
|
|
2054
|
+
def _install_registration_refcounts(
|
|
2055
|
+
self,
|
|
2056
|
+
refcounts: dict[str, int],
|
|
2057
|
+
) -> tuple[str, ...]:
|
|
2058
|
+
released = _remove_zero_registration_refcounts(
|
|
2059
|
+
refcounts,
|
|
2060
|
+
self._registration_artifacts,
|
|
2061
|
+
self._quarantine_holds,
|
|
2062
|
+
)
|
|
2063
|
+
self._registration_refcounts = refcounts
|
|
2064
|
+
return released
|
|
2065
|
+
|
|
2066
|
+
def _collect_unretained_generations(self) -> None:
|
|
2067
|
+
for generation, record in tuple(self._generations.items()):
|
|
2068
|
+
if not record.active and not record.explicitly_retained and record.operation_pins == 0:
|
|
2069
|
+
del self._generations[generation]
|
|
2070
|
+
self._handles.pop(generation, None)
|
|
2071
|
+
|
|
2072
|
+
def _quarantine_candidate(self, candidate: WorkerUniverseCandidate) -> None:
|
|
2073
|
+
for artifact in candidate.new_artifacts:
|
|
2074
|
+
self._quarantine_holds.add(
|
|
2075
|
+
registration_name(artifact.logical_name, artifact.artifact_sha256)
|
|
2076
|
+
)
|
|
2077
|
+
|
|
2078
|
+
|
|
2079
|
+
def prepare_worker_root_instruction(
|
|
2080
|
+
candidate: WorkerUniverseCandidate,
|
|
2081
|
+
transaction_id: UUID,
|
|
2082
|
+
previous_root_key: str,
|
|
2083
|
+
) -> str:
|
|
2084
|
+
"""Create and seal a candidate root without changing the active pointer."""
|
|
2085
|
+
if not isinstance(candidate, WorkerUniverseCandidate):
|
|
2086
|
+
raise TypeError("worker universe candidate is required")
|
|
2087
|
+
if type(transaction_id) is not UUID:
|
|
2088
|
+
raise TypeError("worker root transaction id must be a UUID")
|
|
2089
|
+
if previous_root_key != "" and not _safe_identity(previous_root_key):
|
|
2090
|
+
raise ValueError("previous Worker root key is invalid")
|
|
2091
|
+
manifest = candidate.manifest
|
|
2092
|
+
reserved_names = {
|
|
2093
|
+
binding.target_module.casefold()
|
|
2094
|
+
for binding in manifest.wiring
|
|
2095
|
+
if binding.target_kind == "original"
|
|
2096
|
+
}
|
|
2097
|
+
reserved_names.update(
|
|
2098
|
+
module.logical_name.casefold() for module in manifest.modules
|
|
2099
|
+
)
|
|
2100
|
+
|
|
2101
|
+
def allocate_local(preferred: str) -> str:
|
|
2102
|
+
candidate_name = preferred
|
|
2103
|
+
suffix = 0
|
|
2104
|
+
while candidate_name.casefold() in reserved_names:
|
|
2105
|
+
suffix += 1
|
|
2106
|
+
candidate_name = f"{preferred}_{suffix}"
|
|
2107
|
+
reserved_names.add(candidate_name.casefold())
|
|
2108
|
+
return candidate_name
|
|
2109
|
+
|
|
2110
|
+
phase_variable = allocate_local("ЭтапПубликацииWorker")
|
|
2111
|
+
objects_variable = allocate_local("ОбъектыКандидатаWorker")
|
|
2112
|
+
dependency_variables = tuple(
|
|
2113
|
+
(
|
|
2114
|
+
allocate_local(f"ИсточникЗависимостиWorker{index}"),
|
|
2115
|
+
allocate_local(f"ЦельЗависимостиWorker{index}"),
|
|
2116
|
+
)
|
|
2117
|
+
for index, _ in enumerate(manifest.wiring)
|
|
2118
|
+
)
|
|
2119
|
+
module_variables = tuple(
|
|
2120
|
+
allocate_local(f"ОбъектМодуляWorker{index}")
|
|
2121
|
+
for index, _ in enumerate(manifest.modules)
|
|
2122
|
+
)
|
|
2123
|
+
exports_variable = allocate_local("ЭкспортыКандидатаWorker")
|
|
2124
|
+
exports_data_variable = allocate_local("ДанныеЭкспортовКандидатаWorker")
|
|
2125
|
+
modules_variable = allocate_local("МодулиКандидатаWorker")
|
|
2126
|
+
root_data_variable = allocate_local("ДанныеКорняКандидатаWorker")
|
|
2127
|
+
root_variable = allocate_local("КореньКандидатаWorker")
|
|
2128
|
+
prepared_data_variable = allocate_local("ДанныеПодготовленногоКорняWorker")
|
|
2129
|
+
prepared_variable = allocate_local("ПодготовленныйКореньWorker")
|
|
2130
|
+
error_variable = allocate_local("ОшибкаПубликацииWorker")
|
|
2131
|
+
artifact_error_variable = allocate_local("МаркерОшибкиАртефактаWorker")
|
|
2132
|
+
prepare_started_variable = allocate_local("НачалоПодготовкиКандидатаWorker")
|
|
2133
|
+
prepare_finished_variable = allocate_local("КонецПодготовкиКандидатаWorker")
|
|
2134
|
+
lines = [
|
|
2135
|
+
f'{phase_variable} = "create";',
|
|
2136
|
+
f'{artifact_error_variable} = "";',
|
|
2137
|
+
"Попытка",
|
|
2138
|
+
f" {prepare_started_variable} = "
|
|
2139
|
+
"ТекущаяУниверсальнаяДатаВМиллисекундах();",
|
|
2140
|
+
f" {objects_variable} = Новый Соответствие;",
|
|
2141
|
+
]
|
|
2142
|
+
for module in manifest.modules:
|
|
2143
|
+
artifact_error_header = (
|
|
2144
|
+
"onec-worker-artifact-stage="
|
|
2145
|
+
f"artifact_sha256={module.artifact_sha256};"
|
|
2146
|
+
f"logical_name_sha256={sha256(module.logical_name.casefold().encode('utf-8')).hexdigest()};"
|
|
2147
|
+
"phase=create;boundary=create"
|
|
2148
|
+
)
|
|
2149
|
+
lines.append(f" {artifact_error_variable} = {_bsl_string(artifact_error_header)};")
|
|
2150
|
+
lines.append(
|
|
2151
|
+
f" {objects_variable}.Вставить("
|
|
2152
|
+
f"{_bsl_string(module.logical_name)}, ВнешниеОбработки.Создать("
|
|
2153
|
+
f"{_bsl_string(module.registration_name)}, Ложь));"
|
|
2154
|
+
)
|
|
2155
|
+
lines.append(f' {artifact_error_variable} = "";')
|
|
2156
|
+
lines.append(f' {phase_variable} = "wire";')
|
|
2157
|
+
for index, binding in enumerate(manifest.wiring):
|
|
2158
|
+
source_variable, target_variable = dependency_variables[index]
|
|
2159
|
+
lines.append(
|
|
2160
|
+
f" {source_variable} = {objects_variable}.Получить("
|
|
2161
|
+
f"{_bsl_string(binding.source_module)});"
|
|
2162
|
+
)
|
|
2163
|
+
if binding.target_kind == "overloaded":
|
|
2164
|
+
lines.append(
|
|
2165
|
+
f" {target_variable} = {objects_variable}.Получить("
|
|
2166
|
+
f"{_bsl_string(binding.target_module)});"
|
|
2167
|
+
)
|
|
2168
|
+
else:
|
|
2169
|
+
lines.append(f" {target_variable} = {binding.target_module};")
|
|
2170
|
+
lines.extend(
|
|
2171
|
+
(
|
|
2172
|
+
f" {source_variable}.{binding.export_variable} = {target_variable};",
|
|
2173
|
+
f" Если ТипЗнч({source_variable}.{binding.export_variable}) "
|
|
2174
|
+
f"<> ТипЗнч({target_variable}) Тогда",
|
|
2175
|
+
' ВызватьИсключение "worker dependency field type mismatch";',
|
|
2176
|
+
" КонецЕсли;",
|
|
2177
|
+
)
|
|
2178
|
+
)
|
|
2179
|
+
lines.append(f' {phase_variable} = "probe";')
|
|
2180
|
+
for index, module in enumerate(manifest.modules):
|
|
2181
|
+
object_variable = module_variables[index]
|
|
2182
|
+
lines.extend(
|
|
2183
|
+
(
|
|
2184
|
+
f" {object_variable} = {objects_variable}.Получить("
|
|
2185
|
+
f"{_bsl_string(module.logical_name)});",
|
|
2186
|
+
f" Если ТипЗнч({object_variable}) = Тип(\"Неопределено\") Тогда",
|
|
2187
|
+
' ВызватьИсключение "worker module object is unavailable";',
|
|
2188
|
+
" КонецЕсли;",
|
|
2189
|
+
)
|
|
2190
|
+
)
|
|
2191
|
+
lines.append(f" {exports_variable} = Новый Массив;")
|
|
2192
|
+
for export in manifest.exports:
|
|
2193
|
+
lines.append(
|
|
2194
|
+
f" {exports_variable}.Добавить("
|
|
2195
|
+
f"{_bsl_string(export.public_path)});"
|
|
2196
|
+
)
|
|
2197
|
+
root_key = _generation_root_key(manifest.generation)
|
|
2198
|
+
lines.extend(
|
|
2199
|
+
(
|
|
2200
|
+
f" {modules_variable} = Новый ФиксированноеСоответствие("
|
|
2201
|
+
f"{objects_variable});",
|
|
2202
|
+
f" {exports_variable} = Новый ФиксированныйМассив("
|
|
2203
|
+
f"{exports_variable});",
|
|
2204
|
+
f" {exports_data_variable} = Новый Структура;",
|
|
2205
|
+
f' {exports_data_variable}.Вставить("Kind", '
|
|
2206
|
+
'"OnecWorkerExportsV1");',
|
|
2207
|
+
f' {exports_data_variable}.Вставить("Items", '
|
|
2208
|
+
f"{exports_variable});",
|
|
2209
|
+
f" {exports_variable} = Новый ФиксированнаяСтруктура("
|
|
2210
|
+
f"{exports_data_variable});",
|
|
2211
|
+
f" {root_data_variable} = Новый Структура;",
|
|
2212
|
+
f' {root_data_variable}.Вставить("ManifestSha256", '
|
|
2213
|
+
f"{_bsl_string(manifest.sha256)});",
|
|
2214
|
+
f' {root_data_variable}.Вставить("RootKey", '
|
|
2215
|
+
f"{_bsl_string(root_key)});",
|
|
2216
|
+
f' {root_data_variable}.Вставить("Modules", '
|
|
2217
|
+
f"{modules_variable});",
|
|
2218
|
+
f' {root_data_variable}.Вставить("Exports", '
|
|
2219
|
+
f"{exports_variable});",
|
|
2220
|
+
)
|
|
2221
|
+
)
|
|
2222
|
+
lines.extend(
|
|
2223
|
+
(
|
|
2224
|
+
f" {root_variable} = Новый ФиксированнаяСтруктура("
|
|
2225
|
+
f"{root_data_variable});",
|
|
2226
|
+
f" {prepare_finished_variable} = "
|
|
2227
|
+
"ТекущаяУниверсальнаяДатаВМиллисекундах();",
|
|
2228
|
+
"Исключение",
|
|
2229
|
+
f" {error_variable} = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());",
|
|
2230
|
+
f' Если {artifact_error_variable} <> "" Тогда',
|
|
2231
|
+
f' {error_variable} = {artifact_error_variable} + ";diagnostic_utf16_length=" '
|
|
2232
|
+
f'+ Формат(СтрДлина({error_variable}), "ЧГ=0; ЧДЦ=0; ЧН=0") '
|
|
2233
|
+
f"+ Символы.ПС + {error_variable};",
|
|
2234
|
+
" КонецЕсли;",
|
|
2235
|
+
' ВызватьИсключение "onec-worker-root-prepare-stage=" '
|
|
2236
|
+
f"+ {phase_variable} + Символы.ПС + {error_variable};",
|
|
2237
|
+
"КонецПопытки;",
|
|
2238
|
+
f"{prepared_data_variable} = Новый Структура;",
|
|
2239
|
+
f'{prepared_data_variable}.Вставить("TransactionId", '
|
|
2240
|
+
f"{_bsl_string(str(transaction_id))});",
|
|
2241
|
+
f'{prepared_data_variable}.Вставить("Generation", '
|
|
2242
|
+
f"{manifest.generation});",
|
|
2243
|
+
f'{prepared_data_variable}.Вставить("ManifestSha256", '
|
|
2244
|
+
f"{_bsl_string(manifest.sha256)});",
|
|
2245
|
+
f'{prepared_data_variable}.Вставить("CandidateRootKey", '
|
|
2246
|
+
f"{_bsl_string(root_key)});",
|
|
2247
|
+
f'{prepared_data_variable}.Вставить("PreviousRootKey", '
|
|
2248
|
+
f"{_bsl_string(previous_root_key)});",
|
|
2249
|
+
f'{prepared_data_variable}.Вставить("Root", {root_variable});',
|
|
2250
|
+
f'{prepared_data_variable}.Вставить("PrepareMs", '
|
|
2251
|
+
f"{prepare_finished_variable} - {prepare_started_variable});",
|
|
2252
|
+
f"{prepared_variable} = Новый ФиксированнаяСтруктура("
|
|
2253
|
+
f"{prepared_data_variable});",
|
|
2254
|
+
f"Контекст.Вставить({_bsl_string(_prepared_root_context_key(transaction_id))}, "
|
|
2255
|
+
f"{prepared_variable});",
|
|
2256
|
+
"Результат = "
|
|
2257
|
+
f"{_bsl_string(_PREPARED_ROOT_RECEIPT_PREFIX + '|' + str(transaction_id) + '|')} + "
|
|
2258
|
+
f'Формат({manifest.generation}, "ЧГ=0; ЧДЦ=0; ЧН=0") + "|" + '
|
|
2259
|
+
f"{_bsl_string(manifest.sha256 + '|' + root_key + '|' + _wire_root_key(previous_root_key) + '|')} + "
|
|
2260
|
+
f"Формат({prepare_finished_variable} - {prepare_started_variable}, "
|
|
2261
|
+
'"ЧГ=0; ЧДЦ=0; ЧН=0");',
|
|
2262
|
+
)
|
|
2263
|
+
)
|
|
2264
|
+
return "\n".join(lines)
|
|
2265
|
+
|
|
2266
|
+
|
|
2267
|
+
def swap_worker_root_instruction(prepared: WorkerPreparedRootReceipt) -> str:
|
|
2268
|
+
"""Guard and publish one exact prepared root as the active root."""
|
|
2269
|
+
if type(prepared) is not WorkerPreparedRootReceipt:
|
|
2270
|
+
raise TypeError("worker prepared root receipt is required")
|
|
2271
|
+
prepared_key = _prepared_root_context_key(prepared.transaction_id)
|
|
2272
|
+
outcome_key = _root_swap_outcome_context_key(prepared.transaction_id)
|
|
2273
|
+
previous = prepared.previous_root_key
|
|
2274
|
+
lines = [
|
|
2275
|
+
"Результат = Неопределено;",
|
|
2276
|
+
f"Если Не Контекст.Свойство({_bsl_string(outcome_key)}, Результат) Тогда",
|
|
2277
|
+
" ПодготовленныйКореньWorker = Неопределено;",
|
|
2278
|
+
" ТекущийКореньWorker = Неопределено;",
|
|
2279
|
+
" Попытка",
|
|
2280
|
+
f" Если Не Контекст.Свойство({_bsl_string(prepared_key)}, "
|
|
2281
|
+
"ПодготовленныйКореньWorker) Тогда",
|
|
2282
|
+
' ВызватьИсключение "prepared root is unavailable";',
|
|
2283
|
+
" КонецЕсли;",
|
|
2284
|
+
" Если ПодготовленныйКореньWorker.TransactionId <> "
|
|
2285
|
+
f"{_bsl_string(str(prepared.transaction_id))} Или "
|
|
2286
|
+
"ПодготовленныйКореньWorker.Generation <> "
|
|
2287
|
+
f"{prepared.generation} Или "
|
|
2288
|
+
"ПодготовленныйКореньWorker.ManifestSha256 <> "
|
|
2289
|
+
f"{_bsl_string(prepared.manifest_sha256)} Или "
|
|
2290
|
+
"ПодготовленныйКореньWorker.CandidateRootKey <> "
|
|
2291
|
+
f"{_bsl_string(prepared.candidate_root_key)} Или "
|
|
2292
|
+
"ПодготовленныйКореньWorker.PreviousRootKey <> "
|
|
2293
|
+
f"{_bsl_string(previous)} Тогда",
|
|
2294
|
+
' ВызватьИсключение "prepared root identity mismatch";',
|
|
2295
|
+
" КонецЕсли;",
|
|
2296
|
+
]
|
|
2297
|
+
if previous:
|
|
2298
|
+
lines.extend(
|
|
2299
|
+
(
|
|
2300
|
+
' Если Не Контекст.Свойство("RuntimeWorkerActiveGeneration", '
|
|
2301
|
+
"ТекущийКореньWorker) Или ТекущийКореньWorker.RootKey <> "
|
|
2302
|
+
f"{_bsl_string(previous)} Тогда",
|
|
2303
|
+
' ВызватьИсключение "previous root identity mismatch";',
|
|
2304
|
+
" КонецЕсли;",
|
|
2305
|
+
)
|
|
2306
|
+
)
|
|
2307
|
+
else:
|
|
2308
|
+
lines.extend(
|
|
2309
|
+
(
|
|
2310
|
+
' Если Контекст.Свойство("RuntimeWorkerActiveGeneration", '
|
|
2311
|
+
"ТекущийКореньWorker) Тогда",
|
|
2312
|
+
' ВызватьИсключение "unexpected previous root";',
|
|
2313
|
+
" КонецЕсли;",
|
|
2314
|
+
)
|
|
2315
|
+
)
|
|
2316
|
+
lines.extend(
|
|
2317
|
+
(
|
|
2318
|
+
" Исключение",
|
|
2319
|
+
" ОшибкаСменыКорняWorker = ОписаниеОшибки();",
|
|
2320
|
+
' ВызватьИсключение "onec-worker-root-swap-stage=guard" '
|
|
2321
|
+
"+ Символы.ПС + ОшибкаСменыКорняWorker;",
|
|
2322
|
+
" КонецПопытки;",
|
|
2323
|
+
" НачалоСменыКорняWorker = "
|
|
2324
|
+
"ТекущаяУниверсальнаяДатаВМиллисекундах();",
|
|
2325
|
+
' Контекст.Вставить("RuntimeWorkerActiveGeneration", '
|
|
2326
|
+
"ПодготовленныйКореньWorker.Root);",
|
|
2327
|
+
f" Контекст.Удалить({_bsl_string(prepared_key)});",
|
|
2328
|
+
" КонецСменыКорняWorker = "
|
|
2329
|
+
"ТекущаяУниверсальнаяДатаВМиллисекундах();",
|
|
2330
|
+
" Результат = "
|
|
2331
|
+
f"{_bsl_string(_ROOT_SWAP_RECEIPT_PREFIX + '|' + str(prepared.transaction_id) + '|')} + "
|
|
2332
|
+
f'Формат({prepared.generation}, "ЧГ=0; ЧДЦ=0; ЧН=0") + "|" + '
|
|
2333
|
+
f"{_bsl_string(prepared.manifest_sha256 + '|' + prepared.candidate_root_key + '|' + _wire_root_key(previous) + '|1|')} + "
|
|
2334
|
+
"Формат(ПодготовленныйКореньWorker.PrepareMs, "
|
|
2335
|
+
'"ЧГ=0; ЧДЦ=0; ЧН=0") + "|" + '
|
|
2336
|
+
"Формат(КонецСменыКорняWorker - НачалоСменыКорняWorker, "
|
|
2337
|
+
'"ЧГ=0; ЧДЦ=0; ЧН=0");',
|
|
2338
|
+
f" Контекст.Вставить({_bsl_string(outcome_key)}, Результат);",
|
|
2339
|
+
"КонецЕсли;",
|
|
2340
|
+
)
|
|
2341
|
+
)
|
|
2342
|
+
return "\n".join(lines)
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
def discard_worker_root_instruction(prepared: WorkerPreparedRootReceipt) -> str:
|
|
2346
|
+
"""Discard an exact prepared root without touching the active pointer."""
|
|
2347
|
+
if type(prepared) is not WorkerPreparedRootReceipt:
|
|
2348
|
+
raise TypeError("worker prepared root receipt is required")
|
|
2349
|
+
prepared_key = _prepared_root_context_key(prepared.transaction_id)
|
|
2350
|
+
outcome_key = _root_discard_outcome_context_key(prepared.transaction_id)
|
|
2351
|
+
return "\n".join(
|
|
2352
|
+
(
|
|
2353
|
+
"Результат = Неопределено;",
|
|
2354
|
+
f"Если Не Контекст.Свойство({_bsl_string(outcome_key)}, Результат) Тогда",
|
|
2355
|
+
" ПодготовленныйКореньWorker = Неопределено;",
|
|
2356
|
+
" Попытка",
|
|
2357
|
+
f" Если Не Контекст.Свойство({_bsl_string(prepared_key)}, "
|
|
2358
|
+
"ПодготовленныйКореньWorker) Тогда",
|
|
2359
|
+
' ВызватьИсключение "prepared root is unavailable";',
|
|
2360
|
+
" КонецЕсли;",
|
|
2361
|
+
" Если ПодготовленныйКореньWorker.TransactionId <> "
|
|
2362
|
+
f"{_bsl_string(str(prepared.transaction_id))} Или "
|
|
2363
|
+
"ПодготовленныйКореньWorker.Generation <> "
|
|
2364
|
+
f"{prepared.generation} Или "
|
|
2365
|
+
"ПодготовленныйКореньWorker.ManifestSha256 <> "
|
|
2366
|
+
f"{_bsl_string(prepared.manifest_sha256)} Тогда",
|
|
2367
|
+
' ВызватьИсключение "prepared root identity mismatch";',
|
|
2368
|
+
" КонецЕсли;",
|
|
2369
|
+
" Исключение",
|
|
2370
|
+
" ОшибкаУдаленияКорняWorker = ОписаниеОшибки();",
|
|
2371
|
+
' ВызватьИсключение "onec-worker-root-discard-stage=guard" '
|
|
2372
|
+
"+ Символы.ПС + ОшибкаУдаленияКорняWorker;",
|
|
2373
|
+
" КонецПопытки;",
|
|
2374
|
+
f" Контекст.Удалить({_bsl_string(prepared_key)});",
|
|
2375
|
+
" Результат = "
|
|
2376
|
+
f"{_bsl_string(_ROOT_DISCARD_RECEIPT_PREFIX + '|' + str(prepared.transaction_id) + '|' + str(prepared.generation) + '|' + prepared.manifest_sha256 + '|' + prepared.candidate_root_key)};",
|
|
2377
|
+
f" Контекст.Вставить({_bsl_string(outcome_key)}, Результат);",
|
|
2378
|
+
"КонецЕсли;",
|
|
2379
|
+
)
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def _wire_root_key(value: str) -> str:
|
|
2384
|
+
return "-" if value == "" else value
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
def _unwire_root_key(value: str) -> str:
|
|
2388
|
+
return "" if value == "-" else value
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
def _prepared_root_context_key(transaction_id: UUID) -> str:
|
|
2392
|
+
return f"__OnecWorkerPrepared_{transaction_id.hex}"
|
|
2393
|
+
|
|
2394
|
+
|
|
2395
|
+
def _root_swap_outcome_context_key(transaction_id: UUID) -> str:
|
|
2396
|
+
return f"__OnecWorkerSwapOutcome_{transaction_id.hex}"
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
def _root_discard_outcome_context_key(transaction_id: UUID) -> str:
|
|
2400
|
+
return f"__OnecWorkerDiscardOutcome_{transaction_id.hex}"
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
def _bsl_string(value: str) -> str:
|
|
2404
|
+
return '"' + value.replace('"', '""') + '"'
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
2408
|
+
class WorkerRegistrationRecord:
|
|
2409
|
+
target_incarnation_id: UUID
|
|
2410
|
+
canonical_module: str
|
|
2411
|
+
artifact_sha256: str
|
|
2412
|
+
registration_name: str
|
|
2413
|
+
exact_temp_storage_url: str
|
|
2414
|
+
locator_profile_id: str
|
|
2415
|
+
stage_transaction_id: UUID
|
|
2416
|
+
batch_digest: str
|
|
2417
|
+
platform_build: str
|
|
2418
|
+
receipt_admission: object
|
|
2419
|
+
|
|
2420
|
+
def __post_init__(self) -> None:
|
|
2421
|
+
if (
|
|
2422
|
+
type(self.target_incarnation_id) is not UUID
|
|
2423
|
+
or not _safe_identity(self.canonical_module)
|
|
2424
|
+
or self.canonical_module != self.canonical_module.casefold()
|
|
2425
|
+
or not _sha256_value(self.artifact_sha256)
|
|
2426
|
+
or not _bsl_identifier(self.registration_name)
|
|
2427
|
+
or _temp_storage_session_id(self.exact_temp_storage_url) is None
|
|
2428
|
+
or self.locator_profile_id != _WORKER_LOCATOR_PROFILE_ID
|
|
2429
|
+
or type(self.stage_transaction_id) is not UUID
|
|
2430
|
+
or not _sha256_value(self.batch_digest)
|
|
2431
|
+
or type(self.platform_build) is not str
|
|
2432
|
+
or not self.platform_build
|
|
2433
|
+
or self.receipt_admission is not _REGISTRATION_RECEIPT_ADMISSION
|
|
2434
|
+
):
|
|
2435
|
+
raise ValueError("worker registration record is invalid")
|
|
2436
|
+
|
|
2437
|
+
def module_location(self, generated_line: int) -> ModuleLocation:
|
|
2438
|
+
if type(generated_line) is not int or generated_line < 1:
|
|
2439
|
+
raise ValueError("generated line must be positive")
|
|
2440
|
+
if (
|
|
2441
|
+
self.receipt_admission is not _REGISTRATION_RECEIPT_ADMISSION
|
|
2442
|
+
or self.platform_build != _WORKER_LOCATOR_PLATFORM_BUILD
|
|
2443
|
+
):
|
|
2444
|
+
raise ProtocolError("Worker breakpoint locator is unsupported")
|
|
2445
|
+
return ModuleLocation(
|
|
2446
|
+
"ExtMDModule",
|
|
2447
|
+
self.exact_temp_storage_url,
|
|
2448
|
+
_WORKER_MODULE_OBJECT_ID,
|
|
2449
|
+
_WORKER_MODULE_PROPERTY_ID,
|
|
2450
|
+
generated_line,
|
|
2451
|
+
"",
|
|
2452
|
+
0,
|
|
2453
|
+
)
|
|
2454
|
+
|
|
2455
|
+
def __repr__(self) -> str:
|
|
2456
|
+
return (
|
|
2457
|
+
"WorkerRegistrationRecord("
|
|
2458
|
+
f"target_incarnation_id={self.target_incarnation_id!r}, "
|
|
2459
|
+
f"canonical_module={self.canonical_module!r}, "
|
|
2460
|
+
f"artifact_sha256={self.artifact_sha256!r}, "
|
|
2461
|
+
f"registration_name={self.registration_name!r}, "
|
|
2462
|
+
"exact_temp_storage_url=<redacted>, "
|
|
2463
|
+
f"locator_profile_id={self.locator_profile_id!r}, "
|
|
2464
|
+
f"stage_transaction_id={self.stage_transaction_id!r}, "
|
|
2465
|
+
f"batch_digest={self.batch_digest!r}, "
|
|
2466
|
+
f"platform_build={self.platform_build!r}, "
|
|
2467
|
+
"receipt_admission=<redacted>)"
|
|
2468
|
+
)
|
|
2469
|
+
|
|
2470
|
+
|
|
2471
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
2472
|
+
class WorkerModuleDebugView:
|
|
2473
|
+
source_units: tuple[SourceUnitRef, ...]
|
|
2474
|
+
canonical_module: str
|
|
2475
|
+
mapped_source: MappedSource
|
|
2476
|
+
visible_context: VisibleSourceContext | None
|
|
2477
|
+
artifact_sha256: str
|
|
2478
|
+
source_map_sha256: str
|
|
2479
|
+
registration: WorkerRegistrationRecord
|
|
2480
|
+
generated_line_index: LineIndex = field(init=False, repr=False, compare=False)
|
|
2481
|
+
|
|
2482
|
+
def __post_init__(self) -> None:
|
|
2483
|
+
if (
|
|
2484
|
+
type(self.source_units) is not tuple
|
|
2485
|
+
or not self.source_units
|
|
2486
|
+
or any(not isinstance(unit, SourceUnitRef) for unit in self.source_units)
|
|
2487
|
+
or not _safe_identity(self.canonical_module)
|
|
2488
|
+
or self.canonical_module != self.canonical_module.casefold()
|
|
2489
|
+
or not isinstance(self.mapped_source, MappedSource)
|
|
2490
|
+
or (
|
|
2491
|
+
self.visible_context is not None
|
|
2492
|
+
and not isinstance(self.visible_context, VisibleSourceContext)
|
|
2493
|
+
)
|
|
2494
|
+
or not _sha256_value(self.artifact_sha256)
|
|
2495
|
+
or not _sha256_value(self.source_map_sha256)
|
|
2496
|
+
or self.mapped_source.source_map_sha256 != self.source_map_sha256
|
|
2497
|
+
or type(self.registration) is not WorkerRegistrationRecord
|
|
2498
|
+
or self.registration.canonical_module != self.canonical_module
|
|
2499
|
+
or self.registration.artifact_sha256 != self.artifact_sha256
|
|
2500
|
+
):
|
|
2501
|
+
raise ValueError("worker module debug view is invalid")
|
|
2502
|
+
if self.source_units != _worker_debug_source_units(self.mapped_source):
|
|
2503
|
+
raise ValueError("worker debug sources do not match the mapped source")
|
|
2504
|
+
object.__setattr__(
|
|
2505
|
+
self,
|
|
2506
|
+
"generated_line_index",
|
|
2507
|
+
LineIndex(self.mapped_source.text),
|
|
2508
|
+
)
|
|
2509
|
+
|
|
2510
|
+
@property
|
|
2511
|
+
def source_unit(self) -> SourceUnitRef:
|
|
2512
|
+
"""Compatibility accessor for a module with one proven visible source."""
|
|
2513
|
+
if len(self.source_units) != 1:
|
|
2514
|
+
raise ProtocolError("Worker debug source identity is ambiguous")
|
|
2515
|
+
return self.source_units[0]
|
|
2516
|
+
|
|
2517
|
+
def __repr__(self) -> str:
|
|
2518
|
+
return (
|
|
2519
|
+
"WorkerModuleDebugView("
|
|
2520
|
+
f"source_units={self.source_units!r}, "
|
|
2521
|
+
f"canonical_module={self.canonical_module!r}, "
|
|
2522
|
+
f"artifact_sha256={self.artifact_sha256!r}, "
|
|
2523
|
+
f"source_map_sha256={self.source_map_sha256!r}, "
|
|
2524
|
+
"mapped_source=<redacted>, visible_context=<redacted>, "
|
|
2525
|
+
"registration=<redacted>)"
|
|
2526
|
+
)
|
|
2527
|
+
|
|
2528
|
+
|
|
2529
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
2530
|
+
class WorkerGenerationDebugView:
|
|
2531
|
+
handle: WorkerGenerationHandle
|
|
2532
|
+
manifest: WorkerUniverseManifest
|
|
2533
|
+
modules: tuple[WorkerModuleDebugView, ...]
|
|
2534
|
+
|
|
2535
|
+
def __post_init__(self) -> None:
|
|
2536
|
+
if (
|
|
2537
|
+
not isinstance(self.handle, WorkerGenerationHandle)
|
|
2538
|
+
or not isinstance(self.manifest, WorkerUniverseManifest)
|
|
2539
|
+
or self.handle.generation != self.manifest.generation
|
|
2540
|
+
or self.handle.manifest_sha256 != self.manifest.sha256
|
|
2541
|
+
or type(self.modules) is not tuple
|
|
2542
|
+
or len(self.modules) != len(self.manifest.modules)
|
|
2543
|
+
or any(type(module) is not WorkerModuleDebugView for module in self.modules)
|
|
2544
|
+
or any(
|
|
2545
|
+
module.canonical_module != descriptor.logical_name.casefold()
|
|
2546
|
+
or module.artifact_sha256 != descriptor.artifact_sha256
|
|
2547
|
+
or module.registration.registration_name
|
|
2548
|
+
!= descriptor.registration_name
|
|
2549
|
+
for module, descriptor in zip(
|
|
2550
|
+
self.modules,
|
|
2551
|
+
self.manifest.modules,
|
|
2552
|
+
strict=True,
|
|
2553
|
+
)
|
|
2554
|
+
)
|
|
2555
|
+
):
|
|
2556
|
+
raise ValueError("worker generation debug view is invalid")
|
|
2557
|
+
|
|
2558
|
+
def __repr__(self) -> str:
|
|
2559
|
+
return (
|
|
2560
|
+
"WorkerGenerationDebugView("
|
|
2561
|
+
f"handle={self.handle!r}, manifest_sha256={self.manifest.sha256!r}, "
|
|
2562
|
+
f"modules={len(self.modules)}, provenance=<redacted>)"
|
|
2563
|
+
)
|
|
2564
|
+
|
|
2565
|
+
|
|
2566
|
+
def _worker_debug_source_units(
|
|
2567
|
+
mapped_source: MappedSource,
|
|
2568
|
+
) -> tuple[SourceUnitRef, ...]:
|
|
2569
|
+
"""Enumerate exact identities from an admitted flattened map, without aliases."""
|
|
2570
|
+
compact_spans = getattr(mapped_source.source_map, "compact_visible_spans", None)
|
|
2571
|
+
references = (
|
|
2572
|
+
tuple(reference for _role, reference, _span in compact_spans())
|
|
2573
|
+
if callable(compact_spans)
|
|
2574
|
+
else tuple(
|
|
2575
|
+
reference
|
|
2576
|
+
for segment in mapped_source.source_map.segments
|
|
2577
|
+
for reference in (segment.origin_ref, segment.anchor_ref)
|
|
2578
|
+
)
|
|
2579
|
+
)
|
|
2580
|
+
units: dict[tuple[object, str, int], SourceUnitRef] = {}
|
|
2581
|
+
for reference in references:
|
|
2582
|
+
if reference is None:
|
|
2583
|
+
continue
|
|
2584
|
+
if not isinstance(reference, SourceUnitRef):
|
|
2585
|
+
raise ProtocolError("Worker debug source map is not flattened")
|
|
2586
|
+
key = (reference.kind, reference.unit_id, reference.revision)
|
|
2587
|
+
existing = units.get(key)
|
|
2588
|
+
if existing is not None and existing.source_sha256 != reference.source_sha256:
|
|
2589
|
+
raise ProtocolError("Worker debug sources have conflicting source identities")
|
|
2590
|
+
units[key] = reference
|
|
2591
|
+
return tuple(units.values())
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
class ServerWorkerUniverseRegistry:
|
|
2595
|
+
"""Target-side counterpart for a host Worker universe registry."""
|
|
2596
|
+
|
|
2597
|
+
def __init__(
|
|
2598
|
+
self,
|
|
2599
|
+
host: WorkerUniverseRegistry,
|
|
2600
|
+
instruction_executor: Callable[[str], object],
|
|
2601
|
+
*,
|
|
2602
|
+
target_incarnation_id: UUID | None = None,
|
|
2603
|
+
platform_build: str = _WORKER_LOCATOR_PLATFORM_BUILD,
|
|
2604
|
+
) -> None:
|
|
2605
|
+
if not isinstance(host, WorkerUniverseRegistry):
|
|
2606
|
+
raise TypeError("host Worker universe registry is required")
|
|
2607
|
+
if not callable(instruction_executor):
|
|
2608
|
+
raise TypeError("worker universe instruction executor must be callable")
|
|
2609
|
+
if target_incarnation_id is not None and type(target_incarnation_id) is not UUID:
|
|
2610
|
+
raise TypeError("target incarnation ID must be a UUID")
|
|
2611
|
+
if type(platform_build) is not str or not platform_build:
|
|
2612
|
+
raise TypeError("platform build must be a non-empty string")
|
|
2613
|
+
self._host = host
|
|
2614
|
+
self._instruction_executor = instruction_executor
|
|
2615
|
+
self._target_incarnation_id = target_incarnation_id or uuid4()
|
|
2616
|
+
self._platform_build = platform_build
|
|
2617
|
+
self._lock = RLock()
|
|
2618
|
+
# Exact 8.3.27 has no supported external-processing Disconnect API.
|
|
2619
|
+
# Keep every successful content identity for this target session,
|
|
2620
|
+
# including identities whose host generation refs reached zero.
|
|
2621
|
+
self._registrations: dict[str, WorkerRegistrationRecord] = {}
|
|
2622
|
+
self._candidate_registrations: dict[int, set[str]] = {}
|
|
2623
|
+
self._instantiated_registrations: set[str] = set()
|
|
2624
|
+
self._compile_failed_registrations: set[str] = set()
|
|
2625
|
+
self._prepared_roots: dict[
|
|
2626
|
+
UUID,
|
|
2627
|
+
tuple[WorkerUniverseCandidate, WorkerPreparedRootReceipt],
|
|
2628
|
+
] = {}
|
|
2629
|
+
self._storage_session_id: str | None = None
|
|
2630
|
+
self._orphan_urls: set[str] = set()
|
|
2631
|
+
self._broken = False
|
|
2632
|
+
|
|
2633
|
+
def prepare(
|
|
2634
|
+
self,
|
|
2635
|
+
artifacts: tuple[WorkerModuleArtifact, ...],
|
|
2636
|
+
) -> WorkerUniverseCandidate:
|
|
2637
|
+
with self._lock:
|
|
2638
|
+
if self._broken:
|
|
2639
|
+
raise ProtocolError("Worker target registry is broken")
|
|
2640
|
+
candidate = self._host.prepare(artifacts)
|
|
2641
|
+
try:
|
|
2642
|
+
activation = self._sealed_activation(candidate)
|
|
2643
|
+
except BaseException:
|
|
2644
|
+
try:
|
|
2645
|
+
self._host._discard_exact_pending_without_fence(candidate)
|
|
2646
|
+
except BaseException:
|
|
2647
|
+
self._break_pending(candidate)
|
|
2648
|
+
raise _promotion_unknown(candidate) from None
|
|
2649
|
+
self._candidate_registrations.pop(
|
|
2650
|
+
candidate.handle.generation,
|
|
2651
|
+
None,
|
|
2652
|
+
)
|
|
2653
|
+
raise
|
|
2654
|
+
try:
|
|
2655
|
+
self._stage_candidate(
|
|
2656
|
+
candidate,
|
|
2657
|
+
batches=self._stage_batches_from_activation(activation),
|
|
2658
|
+
)
|
|
2659
|
+
debug_view = self._build_candidate_debug_view(activation)
|
|
2660
|
+
if debug_view is not None:
|
|
2661
|
+
self._host._bind_candidate_debug_view(candidate, debug_view)
|
|
2662
|
+
except BaseException:
|
|
2663
|
+
if self._host.state is WorkerUniverseState.PREPARING:
|
|
2664
|
+
owned = self._candidate_registrations.get(
|
|
2665
|
+
candidate.handle.generation,
|
|
2666
|
+
set(),
|
|
2667
|
+
)
|
|
2668
|
+
if owned:
|
|
2669
|
+
self._break_pending(candidate)
|
|
2670
|
+
raise _promotion_unknown(candidate) from None
|
|
2671
|
+
try:
|
|
2672
|
+
self._host.discard(candidate)
|
|
2673
|
+
except BaseException:
|
|
2674
|
+
self._break_pending(candidate)
|
|
2675
|
+
raise _promotion_unknown(candidate) from None
|
|
2676
|
+
self._candidate_registrations.pop(
|
|
2677
|
+
candidate.handle.generation,
|
|
2678
|
+
None,
|
|
2679
|
+
)
|
|
2680
|
+
raise
|
|
2681
|
+
return candidate
|
|
2682
|
+
|
|
2683
|
+
def stages(
|
|
2684
|
+
self,
|
|
2685
|
+
candidate: WorkerUniverseCandidate,
|
|
2686
|
+
) -> tuple[WorkerArtifactStage, ...]:
|
|
2687
|
+
with self._lock:
|
|
2688
|
+
return self._stages(candidate)
|
|
2689
|
+
|
|
2690
|
+
def _stages(
|
|
2691
|
+
self,
|
|
2692
|
+
candidate: WorkerUniverseCandidate,
|
|
2693
|
+
) -> tuple[WorkerArtifactStage, ...]:
|
|
2694
|
+
return tuple(
|
|
2695
|
+
WorkerArtifactStage(
|
|
2696
|
+
entry.logical_name,
|
|
2697
|
+
entry.artifact_sha256,
|
|
2698
|
+
entry.registration_name,
|
|
2699
|
+
"connect",
|
|
2700
|
+
stage_worker_module_instruction(
|
|
2701
|
+
entry.artifact_bytes,
|
|
2702
|
+
logical_name=entry.logical_name,
|
|
2703
|
+
artifact_sha256=entry.artifact_sha256,
|
|
2704
|
+
registration_name=entry.registration_name,
|
|
2705
|
+
),
|
|
2706
|
+
)
|
|
2707
|
+
for entry in self._unstaged_sealed_entries(candidate)
|
|
2708
|
+
)
|
|
2709
|
+
|
|
2710
|
+
def _unstaged_sealed_entries(
|
|
2711
|
+
self,
|
|
2712
|
+
candidate: WorkerUniverseCandidate,
|
|
2713
|
+
*,
|
|
2714
|
+
profiler: PhaseRecorder | None = None,
|
|
2715
|
+
) -> tuple[_SealedWorkerStageEntry, ...]:
|
|
2716
|
+
return self._unstaged_sealed_entries_from_activation(
|
|
2717
|
+
self._sealed_activation(candidate, profiler=profiler)
|
|
2718
|
+
)
|
|
2719
|
+
|
|
2720
|
+
def _sealed_activation(
|
|
2721
|
+
self,
|
|
2722
|
+
candidate: WorkerUniverseCandidate,
|
|
2723
|
+
*,
|
|
2724
|
+
profiler: PhaseRecorder | None = None,
|
|
2725
|
+
) -> _SealedWorkerActivation:
|
|
2726
|
+
if not isinstance(candidate, WorkerUniverseCandidate):
|
|
2727
|
+
raise TypeError("worker universe candidate is required")
|
|
2728
|
+
if self._broken:
|
|
2729
|
+
raise ProtocolError("Worker target registry is broken")
|
|
2730
|
+
return (
|
|
2731
|
+
self._host._require_sealed_activation(candidate)
|
|
2732
|
+
if profiler is None
|
|
2733
|
+
else profiler.measure(
|
|
2734
|
+
"artifact_stage_sealed_validation",
|
|
2735
|
+
lambda: self._host._require_sealed_activation(candidate),
|
|
2736
|
+
item_count=lambda result: len(result.stage_entries),
|
|
2737
|
+
)
|
|
2738
|
+
)
|
|
2739
|
+
|
|
2740
|
+
def _unstaged_sealed_entries_from_activation(
|
|
2741
|
+
self,
|
|
2742
|
+
activation: _SealedWorkerActivation,
|
|
2743
|
+
) -> tuple[_SealedWorkerStageEntry, ...]:
|
|
2744
|
+
new_artifact_ids = {
|
|
2745
|
+
id(artifact) for artifact in activation.candidate.new_artifacts
|
|
2746
|
+
}
|
|
2747
|
+
unstaged: list[_SealedWorkerStageEntry] = []
|
|
2748
|
+
for entry in activation.stage_entries:
|
|
2749
|
+
expected_owner = (
|
|
2750
|
+
entry.logical_name.casefold(),
|
|
2751
|
+
entry.artifact_sha256,
|
|
2752
|
+
)
|
|
2753
|
+
owner = self._registrations.get(entry.registration_name)
|
|
2754
|
+
if owner is not None:
|
|
2755
|
+
if (
|
|
2756
|
+
type(owner) is not WorkerRegistrationRecord
|
|
2757
|
+
or owner.target_incarnation_id != self._target_incarnation_id
|
|
2758
|
+
or (owner.canonical_module, owner.artifact_sha256)
|
|
2759
|
+
!= expected_owner
|
|
2760
|
+
):
|
|
2761
|
+
raise ProtocolError(
|
|
2762
|
+
"Worker target registration ownership is invalid"
|
|
2763
|
+
)
|
|
2764
|
+
continue
|
|
2765
|
+
if id(entry.descriptor) not in new_artifact_ids:
|
|
2766
|
+
raise ProtocolError("Worker target registration ownership is missing")
|
|
2767
|
+
unstaged.append(entry)
|
|
2768
|
+
return tuple(unstaged)
|
|
2769
|
+
|
|
2770
|
+
def _stage_batches(
|
|
2771
|
+
self,
|
|
2772
|
+
candidate: WorkerUniverseCandidate,
|
|
2773
|
+
*,
|
|
2774
|
+
profiler: PhaseRecorder | None = None,
|
|
2775
|
+
) -> tuple[WorkerStageBatch, ...]:
|
|
2776
|
+
return self._stage_batches_from_activation(
|
|
2777
|
+
self._sealed_activation(candidate, profiler=profiler)
|
|
2778
|
+
)
|
|
2779
|
+
|
|
2780
|
+
def _stage_batches_from_activation(
|
|
2781
|
+
self,
|
|
2782
|
+
activation: _SealedWorkerActivation,
|
|
2783
|
+
) -> tuple[WorkerStageBatch, ...]:
|
|
2784
|
+
return build_worker_stage_batches(
|
|
2785
|
+
tuple(
|
|
2786
|
+
WorkerStageEntry(
|
|
2787
|
+
entry.logical_name,
|
|
2788
|
+
entry.artifact_sha256,
|
|
2789
|
+
entry.registration_name,
|
|
2790
|
+
entry.artifact_bytes,
|
|
2791
|
+
)
|
|
2792
|
+
for entry in self._unstaged_sealed_entries_from_activation(
|
|
2793
|
+
activation,
|
|
2794
|
+
)
|
|
2795
|
+
)
|
|
2796
|
+
)
|
|
2797
|
+
|
|
2798
|
+
def _build_candidate_debug_view(
|
|
2799
|
+
self,
|
|
2800
|
+
activation: _SealedWorkerActivation,
|
|
2801
|
+
) -> WorkerGenerationDebugView | None:
|
|
2802
|
+
modules: list[WorkerModuleDebugView] = []
|
|
2803
|
+
for entry, descriptor in zip(
|
|
2804
|
+
activation.stage_entries,
|
|
2805
|
+
activation.candidate.manifest.modules,
|
|
2806
|
+
strict=True,
|
|
2807
|
+
):
|
|
2808
|
+
registration = self._registration_view(entry.registration_name)
|
|
2809
|
+
snapshot = entry.admitted_snapshot
|
|
2810
|
+
visible_snapshot = snapshot.visible_context_snapshot
|
|
2811
|
+
source_units = _worker_debug_source_units(snapshot.mapped_source)
|
|
2812
|
+
if not source_units:
|
|
2813
|
+
return None
|
|
2814
|
+
modules.append(
|
|
2815
|
+
WorkerModuleDebugView(
|
|
2816
|
+
source_units,
|
|
2817
|
+
descriptor.logical_name.casefold(),
|
|
2818
|
+
snapshot.mapped_source,
|
|
2819
|
+
None if visible_snapshot is None else visible_snapshot.context,
|
|
2820
|
+
descriptor.artifact_sha256,
|
|
2821
|
+
entry.descriptor.source_map_sha256,
|
|
2822
|
+
registration,
|
|
2823
|
+
)
|
|
2824
|
+
)
|
|
2825
|
+
return WorkerGenerationDebugView(
|
|
2826
|
+
activation.candidate.handle,
|
|
2827
|
+
activation.candidate.manifest,
|
|
2828
|
+
tuple(modules),
|
|
2829
|
+
)
|
|
2830
|
+
|
|
2831
|
+
def prepare_root(
|
|
2832
|
+
self,
|
|
2833
|
+
candidate: WorkerUniverseCandidate,
|
|
2834
|
+
*,
|
|
2835
|
+
transaction_id: UUID,
|
|
2836
|
+
profiler: PhaseRecorder | None = None,
|
|
2837
|
+
) -> WorkerPreparedRootReceipt:
|
|
2838
|
+
if type(transaction_id) is not UUID:
|
|
2839
|
+
raise TypeError("worker root transaction id must be a UUID")
|
|
2840
|
+
with self._lock:
|
|
2841
|
+
if self._broken:
|
|
2842
|
+
raise ProtocolError("Worker target registry is broken")
|
|
2843
|
+
if transaction_id in self._prepared_roots:
|
|
2844
|
+
raise ProtocolError("Worker root transaction was already used")
|
|
2845
|
+
activation = self._sealed_activation(candidate, profiler=profiler)
|
|
2846
|
+
try:
|
|
2847
|
+
batches = self._stage_batches_from_activation(activation)
|
|
2848
|
+
except BaseException as error:
|
|
2849
|
+
self._abort_pre_swap(candidate, error)
|
|
2850
|
+
staged_item_count = sum(len(batch.entries) for batch in batches)
|
|
2851
|
+
if profiler is not None and staged_item_count:
|
|
2852
|
+
profiler.measure(
|
|
2853
|
+
"artifact_staging",
|
|
2854
|
+
lambda: self._stage_candidate(
|
|
2855
|
+
candidate,
|
|
2856
|
+
batches=batches,
|
|
2857
|
+
profiler=profiler,
|
|
2858
|
+
),
|
|
2859
|
+
item_count=lambda _result: staged_item_count,
|
|
2860
|
+
)
|
|
2861
|
+
else:
|
|
2862
|
+
self._stage_candidate(
|
|
2863
|
+
candidate,
|
|
2864
|
+
batches=batches,
|
|
2865
|
+
profiler=profiler,
|
|
2866
|
+
)
|
|
2867
|
+
if activation.debug_view is None:
|
|
2868
|
+
try:
|
|
2869
|
+
debug_view = self._build_candidate_debug_view(activation)
|
|
2870
|
+
if debug_view is not None:
|
|
2871
|
+
self._host._bind_candidate_debug_view(candidate, debug_view)
|
|
2872
|
+
except BaseException as error:
|
|
2873
|
+
self._abort_pre_swap(candidate, error)
|
|
2874
|
+
previous_root_key = self._host.active_root_key or ""
|
|
2875
|
+
try:
|
|
2876
|
+
result = self._instruction_executor(
|
|
2877
|
+
prepare_worker_root_instruction(
|
|
2878
|
+
candidate,
|
|
2879
|
+
transaction_id,
|
|
2880
|
+
previous_root_key,
|
|
2881
|
+
)
|
|
2882
|
+
)
|
|
2883
|
+
except BaseException as error:
|
|
2884
|
+
if _worker_promotion_failure_phase(error) is not None:
|
|
2885
|
+
self._record_uninstantiable_candidate_module(candidate, error)
|
|
2886
|
+
self._abort_pre_swap(candidate, error)
|
|
2887
|
+
self._break_pending(candidate)
|
|
2888
|
+
raise _promotion_unknown(candidate) from None
|
|
2889
|
+
try:
|
|
2890
|
+
receipt = _worker_prepared_root_receipt(result)
|
|
2891
|
+
if (
|
|
2892
|
+
receipt.transaction_id != transaction_id
|
|
2893
|
+
or receipt.generation != candidate.handle.generation
|
|
2894
|
+
or receipt.manifest_sha256 != candidate.manifest.sha256
|
|
2895
|
+
or receipt.candidate_root_key
|
|
2896
|
+
!= _generation_root_key(candidate.handle.generation)
|
|
2897
|
+
or receipt.previous_root_key != previous_root_key
|
|
2898
|
+
):
|
|
2899
|
+
raise ProtocolError("Worker prepared root receipt is stale")
|
|
2900
|
+
except BaseException:
|
|
2901
|
+
self._break_pending(candidate)
|
|
2902
|
+
raise _promotion_unknown(candidate) from None
|
|
2903
|
+
self._prepared_roots[transaction_id] = (candidate, receipt)
|
|
2904
|
+
instantiated = {
|
|
2905
|
+
module.registration_name for module in candidate.manifest.modules
|
|
2906
|
+
}
|
|
2907
|
+
self._instantiated_registrations.update(instantiated)
|
|
2908
|
+
self._compile_failed_registrations.difference_update(instantiated)
|
|
2909
|
+
if profiler is not None:
|
|
2910
|
+
profiler.record_duration(
|
|
2911
|
+
"generation_create_wire_probe",
|
|
2912
|
+
wall_ns=receipt.generation_create_wire_probe_ms * 1_000_000,
|
|
2913
|
+
item_count=1,
|
|
2914
|
+
)
|
|
2915
|
+
return receipt
|
|
2916
|
+
|
|
2917
|
+
def swap_root(
|
|
2918
|
+
self,
|
|
2919
|
+
prepared: WorkerPreparedRootReceipt,
|
|
2920
|
+
*,
|
|
2921
|
+
profiler: PhaseRecorder | None = None,
|
|
2922
|
+
) -> WorkerGenerationHandle:
|
|
2923
|
+
with self._lock:
|
|
2924
|
+
if self._broken:
|
|
2925
|
+
raise ProtocolError("Worker target registry is broken")
|
|
2926
|
+
entry = self._prepared_roots.get(
|
|
2927
|
+
getattr(prepared, "transaction_id", None)
|
|
2928
|
+
)
|
|
2929
|
+
if entry is None or entry[1] is not prepared:
|
|
2930
|
+
raise ProtocolError("Worker prepared root is stale or forged")
|
|
2931
|
+
candidate, _exact_receipt = entry
|
|
2932
|
+
try:
|
|
2933
|
+
result = self._instruction_executor(
|
|
2934
|
+
swap_worker_root_instruction(prepared)
|
|
2935
|
+
)
|
|
2936
|
+
except BaseException as error:
|
|
2937
|
+
if _worker_root_swap_failure_phase(error) is not None:
|
|
2938
|
+
raise
|
|
2939
|
+
self._break_pending(candidate)
|
|
2940
|
+
raise _promotion_unknown(candidate) from None
|
|
2941
|
+
try:
|
|
2942
|
+
receipt = _worker_promotion_receipt(result)
|
|
2943
|
+
if (
|
|
2944
|
+
receipt.transaction_id != prepared.transaction_id
|
|
2945
|
+
or receipt.generation != prepared.generation
|
|
2946
|
+
or receipt.manifest_sha256 != prepared.manifest_sha256
|
|
2947
|
+
or receipt.root_key != prepared.candidate_root_key
|
|
2948
|
+
or receipt.previous_root_key != prepared.previous_root_key
|
|
2949
|
+
or receipt.generation_create_wire_probe_ms
|
|
2950
|
+
!= prepared.generation_create_wire_probe_ms
|
|
2951
|
+
):
|
|
2952
|
+
raise ProtocolError("Worker root swap receipt is stale")
|
|
2953
|
+
confirmation = self._host.confirm(candidate, receipt)
|
|
2954
|
+
except BaseException:
|
|
2955
|
+
self._break_pending(candidate)
|
|
2956
|
+
raise _promotion_unknown(candidate) from None
|
|
2957
|
+
if profiler is not None:
|
|
2958
|
+
profiler.record_duration(
|
|
2959
|
+
"root_swap",
|
|
2960
|
+
wall_ns=receipt.root_swap_ms * 1_000_000,
|
|
2961
|
+
item_count=1,
|
|
2962
|
+
)
|
|
2963
|
+
del self._prepared_roots[prepared.transaction_id]
|
|
2964
|
+
self._candidate_registrations.pop(candidate.handle.generation, None)
|
|
2965
|
+
return confirmation.handle
|
|
2966
|
+
|
|
2967
|
+
def discard_root(self, prepared: WorkerPreparedRootReceipt) -> None:
|
|
2968
|
+
with self._lock:
|
|
2969
|
+
if self._broken:
|
|
2970
|
+
raise ProtocolError("Worker target registry is broken")
|
|
2971
|
+
entry = self._prepared_roots.get(
|
|
2972
|
+
getattr(prepared, "transaction_id", None)
|
|
2973
|
+
)
|
|
2974
|
+
if entry is None or entry[1] is not prepared:
|
|
2975
|
+
raise ProtocolError("Worker prepared root is stale or forged")
|
|
2976
|
+
candidate, _exact_receipt = entry
|
|
2977
|
+
try:
|
|
2978
|
+
result = self._instruction_executor(
|
|
2979
|
+
discard_worker_root_instruction(prepared)
|
|
2980
|
+
)
|
|
2981
|
+
_worker_root_discard_receipt(result, prepared)
|
|
2982
|
+
self._host.discard(candidate)
|
|
2983
|
+
except BaseException:
|
|
2984
|
+
self._break_pending(candidate)
|
|
2985
|
+
raise _promotion_unknown(candidate) from None
|
|
2986
|
+
del self._prepared_roots[prepared.transaction_id]
|
|
2987
|
+
self._candidate_registrations.pop(candidate.handle.generation, None)
|
|
2988
|
+
|
|
2989
|
+
def quarantine_root(self, prepared: WorkerPreparedRootReceipt) -> None:
|
|
2990
|
+
"""Retain both possible roots after another participant becomes unknown."""
|
|
2991
|
+
with self._lock:
|
|
2992
|
+
if self._broken:
|
|
2993
|
+
return
|
|
2994
|
+
entry = self._prepared_roots.get(
|
|
2995
|
+
getattr(prepared, "transaction_id", None)
|
|
2996
|
+
)
|
|
2997
|
+
if entry is None or entry[1] is not prepared:
|
|
2998
|
+
raise ProtocolError("Worker prepared root is stale or forged")
|
|
2999
|
+
candidate, _exact_receipt = entry
|
|
3000
|
+
self._break_pending(candidate)
|
|
3001
|
+
|
|
3002
|
+
def promote(
|
|
3003
|
+
self,
|
|
3004
|
+
candidate: WorkerUniverseCandidate,
|
|
3005
|
+
*,
|
|
3006
|
+
profiler: PhaseRecorder | None = None,
|
|
3007
|
+
) -> WorkerGenerationHandle:
|
|
3008
|
+
prepared = self.prepare_root(
|
|
3009
|
+
candidate,
|
|
3010
|
+
transaction_id=uuid4(),
|
|
3011
|
+
profiler=profiler,
|
|
3012
|
+
)
|
|
3013
|
+
try:
|
|
3014
|
+
return self.swap_root(prepared, profiler=profiler)
|
|
3015
|
+
except BaseException as error:
|
|
3016
|
+
if _worker_root_swap_failure_phase(error) is not None:
|
|
3017
|
+
self.discard_root(prepared)
|
|
3018
|
+
raise
|
|
3019
|
+
|
|
3020
|
+
def privacy_registration_snapshot(self) -> tuple[str, ...]:
|
|
3021
|
+
"""Return every exact registration that can still own a Worker object."""
|
|
3022
|
+
|
|
3023
|
+
with self._lock:
|
|
3024
|
+
registrations = tuple(
|
|
3025
|
+
sorted(
|
|
3026
|
+
self._registrations.keys() - self._compile_failed_registrations,
|
|
3027
|
+
key=str.casefold,
|
|
3028
|
+
)
|
|
3029
|
+
)
|
|
3030
|
+
if (
|
|
3031
|
+
self._broken
|
|
3032
|
+
or self._host.state is not WorkerUniverseState.READY
|
|
3033
|
+
or self._host.active_handle is None
|
|
3034
|
+
or not registrations
|
|
3035
|
+
or len({name.casefold() for name in registrations})
|
|
3036
|
+
!= len(registrations)
|
|
3037
|
+
):
|
|
3038
|
+
raise ProtocolError(
|
|
3039
|
+
"Worker privacy registration snapshot is unavailable"
|
|
3040
|
+
)
|
|
3041
|
+
for name in registrations:
|
|
3042
|
+
owner = self._registrations.get(name)
|
|
3043
|
+
if (
|
|
3044
|
+
not _bsl_identifier(name)
|
|
3045
|
+
or type(owner) is not WorkerRegistrationRecord
|
|
3046
|
+
or owner.registration_name != name
|
|
3047
|
+
or owner.target_incarnation_id != self._target_incarnation_id
|
|
3048
|
+
):
|
|
3049
|
+
raise ProtocolError(
|
|
3050
|
+
"Worker privacy registration snapshot is invalid"
|
|
3051
|
+
)
|
|
3052
|
+
return registrations
|
|
3053
|
+
|
|
3054
|
+
def _record_uninstantiable_candidate_module(
|
|
3055
|
+
self,
|
|
3056
|
+
candidate: WorkerUniverseCandidate,
|
|
3057
|
+
error: BaseException,
|
|
3058
|
+
) -> None:
|
|
3059
|
+
"""Exclude a proven compile-invalid registration from type probes."""
|
|
3060
|
+
failure = worker_artifact_stage_failure(error)
|
|
3061
|
+
if (
|
|
3062
|
+
failure is None
|
|
3063
|
+
or failure.phase != "create"
|
|
3064
|
+
or not parse_platform_diagnostic(
|
|
3065
|
+
_worker_reload_platform_message(error)
|
|
3066
|
+
).has_compilation_marker
|
|
3067
|
+
):
|
|
3068
|
+
return
|
|
3069
|
+
matches = tuple(
|
|
3070
|
+
module.registration_name for module in candidate.manifest.modules
|
|
3071
|
+
if module.artifact_sha256 == failure.artifact_sha256
|
|
3072
|
+
)
|
|
3073
|
+
if len(matches) == 1 and matches[0] not in self._instantiated_registrations:
|
|
3074
|
+
self._compile_failed_registrations.add(matches[0])
|
|
3075
|
+
|
|
3076
|
+
def _registration_view(self, registration_name: str) -> WorkerRegistrationRecord:
|
|
3077
|
+
with self._lock:
|
|
3078
|
+
if not _bsl_identifier(registration_name):
|
|
3079
|
+
raise ProtocolError("Worker registration identity is invalid")
|
|
3080
|
+
record = self._registrations.get(registration_name)
|
|
3081
|
+
if (
|
|
3082
|
+
type(record) is not WorkerRegistrationRecord
|
|
3083
|
+
or record.target_incarnation_id != self._target_incarnation_id
|
|
3084
|
+
or record.registration_name != registration_name
|
|
3085
|
+
or record.receipt_admission is not _REGISTRATION_RECEIPT_ADMISSION
|
|
3086
|
+
):
|
|
3087
|
+
raise ProtocolError("Worker registration identity is unavailable")
|
|
3088
|
+
return record
|
|
3089
|
+
|
|
3090
|
+
def release(self, handle: WorkerGenerationHandle) -> None:
|
|
3091
|
+
with self._lock:
|
|
3092
|
+
if self._broken:
|
|
3093
|
+
raise ProtocolError("Worker target registry is broken")
|
|
3094
|
+
self._host.release_generation(handle)
|
|
3095
|
+
|
|
3096
|
+
def release_pin(self, pin: OperationGenerationPin) -> None:
|
|
3097
|
+
with self._lock:
|
|
3098
|
+
if self._broken:
|
|
3099
|
+
raise ProtocolError("Worker target registry is broken")
|
|
3100
|
+
self._host.release_pin(pin)
|
|
3101
|
+
|
|
3102
|
+
def preview_release(
|
|
3103
|
+
self,
|
|
3104
|
+
release: WorkerGenerationHandle | OperationGenerationPin,
|
|
3105
|
+
) -> WorkerLifecycleReleasePlan:
|
|
3106
|
+
with self._lock:
|
|
3107
|
+
if self._broken:
|
|
3108
|
+
raise ProtocolError("Worker target registry is broken")
|
|
3109
|
+
return self._host._preview_release(release)
|
|
3110
|
+
|
|
3111
|
+
def commit_release(self, plan: WorkerLifecycleReleasePlan) -> tuple[str, ...]:
|
|
3112
|
+
with self._lock:
|
|
3113
|
+
if self._broken:
|
|
3114
|
+
raise ProtocolError("Worker target registry is broken")
|
|
3115
|
+
return self._host._commit_release(plan)
|
|
3116
|
+
|
|
3117
|
+
def teardown(self) -> None:
|
|
3118
|
+
with self._lock:
|
|
3119
|
+
self._host.teardown()
|
|
3120
|
+
# RuntimeSession owns target-process termination; never dispatch an
|
|
3121
|
+
# unsupported cleanup instruction while forgetting the host ledger.
|
|
3122
|
+
self._registrations.clear()
|
|
3123
|
+
self._candidate_registrations.clear()
|
|
3124
|
+
self._instantiated_registrations.clear()
|
|
3125
|
+
self._compile_failed_registrations.clear()
|
|
3126
|
+
self._prepared_roots.clear()
|
|
3127
|
+
self._orphan_urls.clear()
|
|
3128
|
+
self._storage_session_id = None
|
|
3129
|
+
|
|
3130
|
+
def abandon_target(self) -> None:
|
|
3131
|
+
"""Forget target registrations when the target cannot execute cleanup.
|
|
3132
|
+
|
|
3133
|
+
The caller owns terminating the target transport/process. No target
|
|
3134
|
+
instruction is attempted here: host leases and handles are closed, all
|
|
3135
|
+
target identities are forgotten, and this registry is permanently
|
|
3136
|
+
unusable.
|
|
3137
|
+
"""
|
|
3138
|
+
with self._lock:
|
|
3139
|
+
self._host.teardown()
|
|
3140
|
+
self._registrations.clear()
|
|
3141
|
+
self._candidate_registrations.clear()
|
|
3142
|
+
self._instantiated_registrations.clear()
|
|
3143
|
+
self._compile_failed_registrations.clear()
|
|
3144
|
+
self._prepared_roots.clear()
|
|
3145
|
+
self._orphan_urls.clear()
|
|
3146
|
+
self._storage_session_id = None
|
|
3147
|
+
self._broken = True
|
|
3148
|
+
|
|
3149
|
+
def _stage_candidate(
|
|
3150
|
+
self,
|
|
3151
|
+
candidate: WorkerUniverseCandidate,
|
|
3152
|
+
*,
|
|
3153
|
+
batches: tuple[WorkerStageBatch, ...],
|
|
3154
|
+
profiler: PhaseRecorder | None = None,
|
|
3155
|
+
) -> None:
|
|
3156
|
+
if not batches:
|
|
3157
|
+
return
|
|
3158
|
+
generation = candidate.handle.generation
|
|
3159
|
+
possible_registrations = {
|
|
3160
|
+
entry.registration_name
|
|
3161
|
+
for batch in batches
|
|
3162
|
+
for entry in batch.entries
|
|
3163
|
+
}
|
|
3164
|
+
batch_count = len(batches)
|
|
3165
|
+
transaction_id = uuid4()
|
|
3166
|
+
for batch in batches:
|
|
3167
|
+
def stage_batch() -> None:
|
|
3168
|
+
try:
|
|
3169
|
+
instruction = (
|
|
3170
|
+
stage_worker_batch_instruction(
|
|
3171
|
+
batch,
|
|
3172
|
+
batch_count=batch_count,
|
|
3173
|
+
transaction_id=transaction_id,
|
|
3174
|
+
)
|
|
3175
|
+
if profiler is None
|
|
3176
|
+
else profiler.measure(
|
|
3177
|
+
"artifact_stage_base64",
|
|
3178
|
+
lambda: stage_worker_batch_instruction(
|
|
3179
|
+
batch,
|
|
3180
|
+
batch_count=batch_count,
|
|
3181
|
+
transaction_id=transaction_id,
|
|
3182
|
+
),
|
|
3183
|
+
item_count=lambda _result: len(batch.entries),
|
|
3184
|
+
)
|
|
3185
|
+
)
|
|
3186
|
+
except BaseException as error:
|
|
3187
|
+
self._abort_pre_swap(candidate, error)
|
|
3188
|
+
try:
|
|
3189
|
+
result = (
|
|
3190
|
+
self._instruction_executor(instruction)
|
|
3191
|
+
if profiler is None
|
|
3192
|
+
else profiler.measure(
|
|
3193
|
+
"artifact_stage_executor",
|
|
3194
|
+
lambda: self._instruction_executor(instruction),
|
|
3195
|
+
item_count=lambda _result: len(batch.entries),
|
|
3196
|
+
)
|
|
3197
|
+
)
|
|
3198
|
+
except BaseException:
|
|
3199
|
+
owned = self._candidate_registrations.setdefault(
|
|
3200
|
+
generation,
|
|
3201
|
+
set(),
|
|
3202
|
+
)
|
|
3203
|
+
owned.update(possible_registrations)
|
|
3204
|
+
self._break_pending(candidate)
|
|
3205
|
+
raise _promotion_unknown(candidate) from None
|
|
3206
|
+
try:
|
|
3207
|
+
outcome = parse_worker_stage_batch_outcome(
|
|
3208
|
+
result,
|
|
3209
|
+
batch,
|
|
3210
|
+
batch_count=batch_count,
|
|
3211
|
+
transaction_id=transaction_id,
|
|
3212
|
+
)
|
|
3213
|
+
except BaseException:
|
|
3214
|
+
owned = self._candidate_registrations.setdefault(
|
|
3215
|
+
generation,
|
|
3216
|
+
set(),
|
|
3217
|
+
)
|
|
3218
|
+
owned.update(possible_registrations)
|
|
3219
|
+
self._break_pending(candidate)
|
|
3220
|
+
raise _promotion_unknown(candidate) from None
|
|
3221
|
+
try:
|
|
3222
|
+
self._record_stage_entries(
|
|
3223
|
+
batch,
|
|
3224
|
+
outcome,
|
|
3225
|
+
self._candidate_registrations.setdefault(
|
|
3226
|
+
generation,
|
|
3227
|
+
set(),
|
|
3228
|
+
),
|
|
3229
|
+
)
|
|
3230
|
+
except BaseException:
|
|
3231
|
+
owned = self._candidate_registrations.setdefault(
|
|
3232
|
+
generation,
|
|
3233
|
+
set(),
|
|
3234
|
+
)
|
|
3235
|
+
owned.update(possible_registrations)
|
|
3236
|
+
self._break_pending(candidate)
|
|
3237
|
+
raise _promotion_unknown(candidate) from None
|
|
3238
|
+
if (
|
|
3239
|
+
outcome.failure is not None
|
|
3240
|
+
and outcome.failure.orphan_url is not None
|
|
3241
|
+
):
|
|
3242
|
+
self._orphan_urls.add(outcome.failure.orphan_url)
|
|
3243
|
+
if outcome.failure is None:
|
|
3244
|
+
return
|
|
3245
|
+
if outcome.failure.outcome == "known_pre_swap":
|
|
3246
|
+
self._abort_pre_swap(
|
|
3247
|
+
candidate,
|
|
3248
|
+
self._stage_failure_error(candidate, batch, outcome),
|
|
3249
|
+
)
|
|
3250
|
+
owned = self._candidate_registrations.setdefault(
|
|
3251
|
+
generation,
|
|
3252
|
+
set(),
|
|
3253
|
+
)
|
|
3254
|
+
owned.update(possible_registrations)
|
|
3255
|
+
self._break_pending(candidate)
|
|
3256
|
+
raise _promotion_unknown(candidate) from None
|
|
3257
|
+
|
|
3258
|
+
if profiler is None:
|
|
3259
|
+
stage_batch()
|
|
3260
|
+
else:
|
|
3261
|
+
profiler.measure(
|
|
3262
|
+
"artifact_stage_batch",
|
|
3263
|
+
stage_batch,
|
|
3264
|
+
item_count=lambda _result: len(batch.entries),
|
|
3265
|
+
)
|
|
3266
|
+
|
|
3267
|
+
def _stage_failure_error(
|
|
3268
|
+
self,
|
|
3269
|
+
candidate: WorkerUniverseCandidate,
|
|
3270
|
+
batch: WorkerStageBatch,
|
|
3271
|
+
outcome: WorkerStageBatchOutcome,
|
|
3272
|
+
) -> BslExecutionError:
|
|
3273
|
+
failure = outcome.failure
|
|
3274
|
+
if failure is None:
|
|
3275
|
+
raise ProtocolError("Worker stage failure is missing")
|
|
3276
|
+
entry = batch.entries[failure.item_index]
|
|
3277
|
+
artifacts = tuple(
|
|
3278
|
+
artifact
|
|
3279
|
+
for artifact in self._host._candidate_diagnostics(candidate)
|
|
3280
|
+
if artifact.registration_name == entry.registration_name
|
|
3281
|
+
and artifact.artifact_sha256 == entry.artifact_sha256
|
|
3282
|
+
)
|
|
3283
|
+
diagnostic = remap_worker_stage_diagnostic(
|
|
3284
|
+
parse_platform_diagnostic(failure.diagnostic),
|
|
3285
|
+
artifact_sha256=entry.artifact_sha256,
|
|
3286
|
+
phase=failure.phase,
|
|
3287
|
+
candidate_manifest_sha256=candidate.manifest.sha256,
|
|
3288
|
+
candidate_artifacts=artifacts,
|
|
3289
|
+
)
|
|
3290
|
+
return BslExecutionError(failure.diagnostic, diagnostic=diagnostic)
|
|
3291
|
+
|
|
3292
|
+
def _record_stage_entries(
|
|
3293
|
+
self,
|
|
3294
|
+
batch: WorkerStageBatch,
|
|
3295
|
+
outcome: WorkerStageBatchOutcome,
|
|
3296
|
+
owned: set[str],
|
|
3297
|
+
) -> None:
|
|
3298
|
+
for entry, receipt in zip(
|
|
3299
|
+
batch.entries,
|
|
3300
|
+
outcome.connected,
|
|
3301
|
+
strict=False,
|
|
3302
|
+
):
|
|
3303
|
+
self._record_stage_entry(
|
|
3304
|
+
entry,
|
|
3305
|
+
receipt,
|
|
3306
|
+
transaction_id=outcome.transaction_id,
|
|
3307
|
+
batch_digest=outcome.batch_digest,
|
|
3308
|
+
)
|
|
3309
|
+
owned.add(entry.registration_name)
|
|
3310
|
+
|
|
3311
|
+
def _record_stage_entry(
|
|
3312
|
+
self,
|
|
3313
|
+
entry: WorkerStageEntry,
|
|
3314
|
+
receipt: WorkerStageRegistrationReceipt,
|
|
3315
|
+
*,
|
|
3316
|
+
transaction_id: UUID,
|
|
3317
|
+
batch_digest: str,
|
|
3318
|
+
) -> None:
|
|
3319
|
+
storage_session_id = _temp_storage_session_id(receipt.temp_storage_url)
|
|
3320
|
+
if storage_session_id is None:
|
|
3321
|
+
raise ProtocolError("Worker registration URL is invalid")
|
|
3322
|
+
if self._storage_session_id is None:
|
|
3323
|
+
self._storage_session_id = storage_session_id
|
|
3324
|
+
elif self._storage_session_id != storage_session_id:
|
|
3325
|
+
raise ProtocolError("Worker registration storage session is inconsistent")
|
|
3326
|
+
record = WorkerRegistrationRecord(
|
|
3327
|
+
self._target_incarnation_id,
|
|
3328
|
+
entry.logical_name.casefold(),
|
|
3329
|
+
entry.artifact_sha256,
|
|
3330
|
+
entry.registration_name,
|
|
3331
|
+
receipt.temp_storage_url,
|
|
3332
|
+
_WORKER_LOCATOR_PROFILE_ID,
|
|
3333
|
+
transaction_id,
|
|
3334
|
+
batch_digest,
|
|
3335
|
+
self._platform_build,
|
|
3336
|
+
_REGISTRATION_RECEIPT_ADMISSION,
|
|
3337
|
+
)
|
|
3338
|
+
previous = self._registrations.get(entry.registration_name)
|
|
3339
|
+
if previous is not None and previous != record:
|
|
3340
|
+
raise ProtocolError("Worker target registration ownership is invalid")
|
|
3341
|
+
self._registrations[entry.registration_name] = record
|
|
3342
|
+
|
|
3343
|
+
def _abort_pre_swap(
|
|
3344
|
+
self,
|
|
3345
|
+
candidate: WorkerUniverseCandidate,
|
|
3346
|
+
error: BaseException,
|
|
3347
|
+
) -> None:
|
|
3348
|
+
# Successful staging remains reusable and privacy-visible for the
|
|
3349
|
+
# target session even though this generation candidate is discarded.
|
|
3350
|
+
self._candidate_registrations.pop(candidate.handle.generation, None)
|
|
3351
|
+
try:
|
|
3352
|
+
self._host.discard(candidate)
|
|
3353
|
+
except BaseException:
|
|
3354
|
+
self._break_pending(candidate)
|
|
3355
|
+
raise _promotion_unknown(candidate) from None
|
|
3356
|
+
raise error
|
|
3357
|
+
|
|
3358
|
+
def _break_pending(self, candidate: WorkerUniverseCandidate) -> None:
|
|
3359
|
+
self._broken = True
|
|
3360
|
+
try:
|
|
3361
|
+
self._host.mark_broken(candidate)
|
|
3362
|
+
except ProtocolError:
|
|
3363
|
+
if self._host.state is not WorkerUniverseState.BROKEN:
|
|
3364
|
+
self._host.mark_broken()
|
|
3365
|
+
|
|
3366
|
+
def _worker_prepared_root_receipt(value: object) -> WorkerPreparedRootReceipt:
|
|
3367
|
+
if type(value) is WorkerPreparedRootReceipt:
|
|
3368
|
+
return value
|
|
3369
|
+
if type(value) is not str:
|
|
3370
|
+
raise ProtocolError("Worker prepared root receipt is invalid")
|
|
3371
|
+
fields = value.split("|")
|
|
3372
|
+
if (
|
|
3373
|
+
len(fields) != 7
|
|
3374
|
+
or any(not field for field in fields)
|
|
3375
|
+
or fields[0] != _PREPARED_ROOT_RECEIPT_PREFIX
|
|
3376
|
+
or fullmatch(r"(?:0|[1-9][0-9]*)", fields[2]) is None
|
|
3377
|
+
or fullmatch(r"(?:0|[1-9][0-9]*)", fields[6]) is None
|
|
3378
|
+
):
|
|
3379
|
+
raise ProtocolError("Worker prepared root receipt is invalid")
|
|
3380
|
+
try:
|
|
3381
|
+
transaction_id = UUID(fields[1])
|
|
3382
|
+
if str(transaction_id) != fields[1]:
|
|
3383
|
+
raise ValueError
|
|
3384
|
+
return WorkerPreparedRootReceipt(
|
|
3385
|
+
transaction_id,
|
|
3386
|
+
int(fields[2]),
|
|
3387
|
+
fields[3],
|
|
3388
|
+
fields[4],
|
|
3389
|
+
_unwire_root_key(fields[5]),
|
|
3390
|
+
int(fields[6]),
|
|
3391
|
+
)
|
|
3392
|
+
except (TypeError, ValueError):
|
|
3393
|
+
raise ProtocolError("Worker prepared root receipt is invalid") from None
|
|
3394
|
+
|
|
3395
|
+
|
|
3396
|
+
def _worker_promotion_receipt(value: object) -> WorkerPromotionReceipt:
|
|
3397
|
+
if isinstance(value, WorkerPromotionReceipt):
|
|
3398
|
+
return value
|
|
3399
|
+
if type(value) is not str:
|
|
3400
|
+
raise ProtocolError("Worker promotion receipt is invalid")
|
|
3401
|
+
fields = value.split("|")
|
|
3402
|
+
if (
|
|
3403
|
+
len(fields) != 9
|
|
3404
|
+
or any(not field for field in fields)
|
|
3405
|
+
or fields[0] != _ROOT_SWAP_RECEIPT_PREFIX
|
|
3406
|
+
or fields[6] != "1"
|
|
3407
|
+
or fullmatch(r"(?:0|[1-9][0-9]*)", fields[2]) is None
|
|
3408
|
+
or fullmatch(r"(?:0|[1-9][0-9]*)", fields[7]) is None
|
|
3409
|
+
or fullmatch(r"(?:0|[1-9][0-9]*)", fields[8]) is None
|
|
3410
|
+
):
|
|
3411
|
+
raise ProtocolError("Worker promotion receipt is invalid")
|
|
3412
|
+
try:
|
|
3413
|
+
transaction_id = UUID(fields[1])
|
|
3414
|
+
if str(transaction_id) != fields[1]:
|
|
3415
|
+
raise ValueError
|
|
3416
|
+
return WorkerPromotionReceipt(
|
|
3417
|
+
transaction_id,
|
|
3418
|
+
int(fields[2]),
|
|
3419
|
+
fields[3],
|
|
3420
|
+
fields[4],
|
|
3421
|
+
_unwire_root_key(fields[5]),
|
|
3422
|
+
True,
|
|
3423
|
+
int(fields[7]),
|
|
3424
|
+
int(fields[8]),
|
|
3425
|
+
)
|
|
3426
|
+
except (TypeError, ValueError):
|
|
3427
|
+
raise ProtocolError("Worker promotion receipt is invalid") from None
|
|
3428
|
+
|
|
3429
|
+
|
|
3430
|
+
def _worker_root_discard_receipt(
|
|
3431
|
+
value: object,
|
|
3432
|
+
prepared: WorkerPreparedRootReceipt,
|
|
3433
|
+
) -> None:
|
|
3434
|
+
expected = "|".join(
|
|
3435
|
+
(
|
|
3436
|
+
_ROOT_DISCARD_RECEIPT_PREFIX,
|
|
3437
|
+
str(prepared.transaction_id),
|
|
3438
|
+
str(prepared.generation),
|
|
3439
|
+
prepared.manifest_sha256,
|
|
3440
|
+
prepared.candidate_root_key,
|
|
3441
|
+
)
|
|
3442
|
+
)
|
|
3443
|
+
if type(value) is not str or not compare_digest(value, expected):
|
|
3444
|
+
raise ProtocolError("Worker root discard receipt is invalid")
|
|
3445
|
+
|
|
3446
|
+
|
|
3447
|
+
def _worker_promotion_failure_phase(error: BaseException) -> str | None:
|
|
3448
|
+
prefix = "onec-worker-root-prepare-stage="
|
|
3449
|
+
message = str(error)
|
|
3450
|
+
marker = message.find(prefix)
|
|
3451
|
+
if marker < 0:
|
|
3452
|
+
return None
|
|
3453
|
+
phase = message[marker + len(prefix) :].splitlines()[0].rstrip("\r").strip()
|
|
3454
|
+
return phase if phase in {"create", "wire", "probe"} else None
|
|
3455
|
+
|
|
3456
|
+
|
|
3457
|
+
def _worker_root_swap_failure_phase(error: BaseException) -> str | None:
|
|
3458
|
+
prefix = "onec-worker-root-swap-stage="
|
|
3459
|
+
message = str(error)
|
|
3460
|
+
marker = message.find(prefix)
|
|
3461
|
+
if marker < 0:
|
|
3462
|
+
return None
|
|
3463
|
+
phase = message[marker + len(prefix) :].splitlines()[0].rstrip("\r").strip()
|
|
3464
|
+
return phase if phase == "guard" else None
|
|
3465
|
+
|
|
3466
|
+
|
|
3467
|
+
def _promotion_unknown(
|
|
3468
|
+
candidate: WorkerUniverseCandidate,
|
|
3469
|
+
) -> WorkerPromotionOutcomeUnknown:
|
|
3470
|
+
return WorkerPromotionOutcomeUnknown(
|
|
3471
|
+
candidate.handle.generation,
|
|
3472
|
+
candidate.manifest.sha256,
|
|
3473
|
+
)
|
|
3474
|
+
|
|
3475
|
+
|
|
3476
|
+
def registration_name(logical_name: str, artifact_sha256: str) -> str:
|
|
3477
|
+
if not _bsl_identifier(logical_name) or not _sha256_value(artifact_sha256):
|
|
3478
|
+
raise ValueError("worker registration identity is invalid")
|
|
3479
|
+
logical_digest = sha256(logical_name.casefold().encode("utf-8")).hexdigest()[:8]
|
|
3480
|
+
value = f"OnecRuntime_{logical_digest}_{artifact_sha256[:16]}"
|
|
3481
|
+
if not _bsl_identifier(value) or len(value) > 80:
|
|
3482
|
+
raise ValueError("worker registration identity is invalid")
|
|
3483
|
+
return value
|
|
3484
|
+
|
|
3485
|
+
|
|
3486
|
+
def _build_worker_manifest(
|
|
3487
|
+
generation: int,
|
|
3488
|
+
artifacts: tuple[WorkerModuleArtifact, ...],
|
|
3489
|
+
) -> tuple[tuple[_ValidatedWorkerModuleArtifact, ...], WorkerUniverseManifest]:
|
|
3490
|
+
if type(artifacts) is not tuple or not artifacts:
|
|
3491
|
+
raise ProtocolError("Worker universe requires a nonempty artifact tuple")
|
|
3492
|
+
try:
|
|
3493
|
+
views = tuple(_validated_worker_module_artifact(artifact) for artifact in artifacts)
|
|
3494
|
+
ordered_views = tuple(
|
|
3495
|
+
sorted(
|
|
3496
|
+
views,
|
|
3497
|
+
key=lambda view: (
|
|
3498
|
+
view.descriptor.logical_name.casefold(),
|
|
3499
|
+
view.descriptor.artifact_sha256,
|
|
3500
|
+
view.descriptor.revision,
|
|
3501
|
+
),
|
|
3502
|
+
)
|
|
3503
|
+
)
|
|
3504
|
+
ordered = tuple(view.descriptor for view in ordered_views)
|
|
3505
|
+
names = tuple(item.logical_name.casefold() for item in ordered)
|
|
3506
|
+
if len(names) != len(set(names)):
|
|
3507
|
+
raise ProtocolError("Worker universe has duplicate logical modules")
|
|
3508
|
+
modules = tuple(
|
|
3509
|
+
ManifestModule(
|
|
3510
|
+
artifact.logical_name,
|
|
3511
|
+
artifact.revision,
|
|
3512
|
+
artifact.artifact_sha256,
|
|
3513
|
+
registration_name(artifact.logical_name, artifact.artifact_sha256),
|
|
3514
|
+
)
|
|
3515
|
+
for artifact in ordered
|
|
3516
|
+
)
|
|
3517
|
+
artifact_by_name = {
|
|
3518
|
+
artifact.logical_name.casefold(): artifact for artifact in ordered
|
|
3519
|
+
}
|
|
3520
|
+
wiring: list[DependencyTarget] = []
|
|
3521
|
+
seen_bindings: set[tuple[str, str]] = set()
|
|
3522
|
+
for artifact in ordered:
|
|
3523
|
+
for binding in artifact.dependency_bindings:
|
|
3524
|
+
binding_key = (
|
|
3525
|
+
artifact.logical_name.casefold(),
|
|
3526
|
+
binding.export_variable.casefold(),
|
|
3527
|
+
)
|
|
3528
|
+
if binding_key in seen_bindings:
|
|
3529
|
+
raise ProtocolError("Worker universe dependency binding is ambiguous")
|
|
3530
|
+
seen_bindings.add(binding_key)
|
|
3531
|
+
overloaded = artifact_by_name.get(binding.target_module.casefold())
|
|
3532
|
+
target_kind: Literal["original", "overloaded"]
|
|
3533
|
+
if overloaded is None:
|
|
3534
|
+
target_kind = "original"
|
|
3535
|
+
target_name = binding.target_module
|
|
3536
|
+
else:
|
|
3537
|
+
target_kind = "overloaded"
|
|
3538
|
+
target_name = overloaded.logical_name
|
|
3539
|
+
wiring.append(
|
|
3540
|
+
DependencyTarget(
|
|
3541
|
+
artifact.logical_name,
|
|
3542
|
+
binding.export_variable,
|
|
3543
|
+
target_kind,
|
|
3544
|
+
target_name,
|
|
3545
|
+
)
|
|
3546
|
+
)
|
|
3547
|
+
wiring_tuple = tuple(sorted(wiring, key=_dependency_target_sort_key))
|
|
3548
|
+
exports = tuple(
|
|
3549
|
+
sorted(
|
|
3550
|
+
(item for artifact in ordered for item in artifact.exports),
|
|
3551
|
+
key=_worker_export_sort_key,
|
|
3552
|
+
)
|
|
3553
|
+
)
|
|
3554
|
+
validate_worker_export_catalog(exports, require_nonempty=True)
|
|
3555
|
+
digest_value = _worker_manifest_sha256(
|
|
3556
|
+
generation,
|
|
3557
|
+
modules,
|
|
3558
|
+
wiring_tuple,
|
|
3559
|
+
exports,
|
|
3560
|
+
)
|
|
3561
|
+
return ordered_views, WorkerUniverseManifest(
|
|
3562
|
+
generation,
|
|
3563
|
+
modules,
|
|
3564
|
+
wiring_tuple,
|
|
3565
|
+
exports,
|
|
3566
|
+
digest_value,
|
|
3567
|
+
)
|
|
3568
|
+
except ProtocolError:
|
|
3569
|
+
raise
|
|
3570
|
+
except Exception:
|
|
3571
|
+
raise ProtocolError("Worker universe manifest admission is invalid") from None
|
|
3572
|
+
|
|
3573
|
+
|
|
3574
|
+
def _worker_manifest_sha256(
|
|
3575
|
+
generation: int,
|
|
3576
|
+
modules: tuple[ManifestModule, ...],
|
|
3577
|
+
wiring: tuple[DependencyTarget, ...],
|
|
3578
|
+
exports: tuple[WorkerExport, ...],
|
|
3579
|
+
) -> str:
|
|
3580
|
+
manifest = {
|
|
3581
|
+
"schema": "onec-worker-universe-manifest-v1",
|
|
3582
|
+
"generation": generation,
|
|
3583
|
+
"modules": [
|
|
3584
|
+
{
|
|
3585
|
+
"logical_name": item.logical_name.casefold(),
|
|
3586
|
+
"revision": item.revision,
|
|
3587
|
+
"artifact_sha256": item.artifact_sha256,
|
|
3588
|
+
"registration_name": item.registration_name,
|
|
3589
|
+
}
|
|
3590
|
+
for item in modules
|
|
3591
|
+
],
|
|
3592
|
+
"wiring": [
|
|
3593
|
+
{
|
|
3594
|
+
"source_module": item.source_module.casefold(),
|
|
3595
|
+
"export_variable": item.export_variable.casefold(),
|
|
3596
|
+
"target_kind": item.target_kind,
|
|
3597
|
+
"target_module": item.target_module.casefold(),
|
|
3598
|
+
}
|
|
3599
|
+
for item in wiring
|
|
3600
|
+
],
|
|
3601
|
+
"exports": [
|
|
3602
|
+
{
|
|
3603
|
+
"public_path": item.public_path.casefold(),
|
|
3604
|
+
"method": item.method.casefold(),
|
|
3605
|
+
"receiver_module": (
|
|
3606
|
+
None
|
|
3607
|
+
if item.receiver_module is None
|
|
3608
|
+
else item.receiver_module.casefold()
|
|
3609
|
+
),
|
|
3610
|
+
}
|
|
3611
|
+
for item in exports
|
|
3612
|
+
],
|
|
3613
|
+
}
|
|
3614
|
+
payload = json.dumps(
|
|
3615
|
+
manifest,
|
|
3616
|
+
ensure_ascii=False,
|
|
3617
|
+
sort_keys=True,
|
|
3618
|
+
separators=(",", ":"),
|
|
3619
|
+
).encode("utf-8")
|
|
3620
|
+
return sha256(payload).hexdigest()
|
|
3621
|
+
|
|
3622
|
+
|
|
3623
|
+
def _manifest_module_sort_key(item: ManifestModule) -> tuple[str, str, int]:
|
|
3624
|
+
return item.logical_name.casefold(), item.artifact_sha256, item.revision
|
|
3625
|
+
|
|
3626
|
+
|
|
3627
|
+
def _dependency_target_sort_key(
|
|
3628
|
+
item: DependencyTarget,
|
|
3629
|
+
) -> tuple[str, str, str, str]:
|
|
3630
|
+
return (
|
|
3631
|
+
item.source_module.casefold(),
|
|
3632
|
+
item.export_variable.casefold(),
|
|
3633
|
+
item.target_kind,
|
|
3634
|
+
item.target_module.casefold(),
|
|
3635
|
+
)
|
|
3636
|
+
|
|
3637
|
+
|
|
3638
|
+
def _worker_export_sort_key(item: WorkerExport) -> tuple[str, str, str]:
|
|
3639
|
+
return (
|
|
3640
|
+
item.public_path.casefold(),
|
|
3641
|
+
item.method.casefold(),
|
|
3642
|
+
"" if item.receiver_module is None else item.receiver_module.casefold(),
|
|
3643
|
+
)
|
|
3644
|
+
|
|
3645
|
+
|
|
3646
|
+
def _effective_export_catalog_matches(
|
|
3647
|
+
manifest: WorkerUniverseManifest,
|
|
3648
|
+
catalog: tuple[WorkerExport, ...],
|
|
3649
|
+
) -> bool:
|
|
3650
|
+
if type(catalog) is not tuple:
|
|
3651
|
+
return False
|
|
3652
|
+
try:
|
|
3653
|
+
admitted = validate_worker_export_catalog(catalog, require_nonempty=True)
|
|
3654
|
+
except (ProtocolError, ValueError):
|
|
3655
|
+
return False
|
|
3656
|
+
if admitted != catalog:
|
|
3657
|
+
return False
|
|
3658
|
+
manifest_receivers = {
|
|
3659
|
+
(export.receiver_module.casefold(), export.method.casefold())
|
|
3660
|
+
for export in manifest.exports
|
|
3661
|
+
if export.receiver_module is not None
|
|
3662
|
+
}
|
|
3663
|
+
catalog_receivers = {
|
|
3664
|
+
(export.receiver_module.casefold(), export.method.casefold())
|
|
3665
|
+
for export in catalog
|
|
3666
|
+
if export.receiver_module is not None
|
|
3667
|
+
}
|
|
3668
|
+
if catalog_receivers != manifest_receivers:
|
|
3669
|
+
return False
|
|
3670
|
+
return all(
|
|
3671
|
+
export.receiver_module is not None
|
|
3672
|
+
and _bsl_identifier(export.receiver_module)
|
|
3673
|
+
and _bsl_identifier(export.method)
|
|
3674
|
+
and (
|
|
3675
|
+
export.receiver_module.casefold(),
|
|
3676
|
+
export.method.casefold(),
|
|
3677
|
+
)
|
|
3678
|
+
in manifest_receivers
|
|
3679
|
+
for export in catalog
|
|
3680
|
+
)
|
|
3681
|
+
|
|
3682
|
+
|
|
3683
|
+
def _adjust_manifest_registrations(
|
|
3684
|
+
refcounts: dict[str, int],
|
|
3685
|
+
manifest: WorkerUniverseManifest,
|
|
3686
|
+
delta: Literal[-1, 1],
|
|
3687
|
+
) -> None:
|
|
3688
|
+
for module in manifest.modules:
|
|
3689
|
+
current = refcounts.get(module.registration_name, 0)
|
|
3690
|
+
updated = current + delta
|
|
3691
|
+
if updated < 0:
|
|
3692
|
+
raise ProtocolError("Worker registration refcount would become negative")
|
|
3693
|
+
refcounts[module.registration_name] = updated
|
|
3694
|
+
|
|
3695
|
+
|
|
3696
|
+
def _remove_zero_registration_refcounts(
|
|
3697
|
+
refcounts: dict[str, int],
|
|
3698
|
+
registration_artifacts: dict[str, tuple[str, str]],
|
|
3699
|
+
quarantine_holds: set[str],
|
|
3700
|
+
) -> tuple[str, ...]:
|
|
3701
|
+
released = tuple(
|
|
3702
|
+
sorted(
|
|
3703
|
+
(
|
|
3704
|
+
name
|
|
3705
|
+
for name, count in refcounts.items()
|
|
3706
|
+
if count == 0 and name not in quarantine_holds
|
|
3707
|
+
),
|
|
3708
|
+
key=str.casefold,
|
|
3709
|
+
)
|
|
3710
|
+
)
|
|
3711
|
+
for name in released:
|
|
3712
|
+
del refcounts[name]
|
|
3713
|
+
registration_artifacts.pop(name, None)
|
|
3714
|
+
return released
|
|
3715
|
+
|
|
3716
|
+
|
|
3717
|
+
def _require_registration_ownership(
|
|
3718
|
+
manifest: WorkerUniverseManifest,
|
|
3719
|
+
registration_artifacts: dict[str, tuple[str, str]],
|
|
3720
|
+
) -> None:
|
|
3721
|
+
for module in manifest.modules:
|
|
3722
|
+
owner = registration_artifacts.get(module.registration_name)
|
|
3723
|
+
expected = (module.logical_name.casefold(), module.artifact_sha256)
|
|
3724
|
+
if owner is not None and owner != expected:
|
|
3725
|
+
raise ProtocolError("Worker registration identity collision")
|
|
3726
|
+
|
|
3727
|
+
|
|
3728
|
+
def _fence_source(value: GenerationFence) -> Callable[[], int]:
|
|
3729
|
+
if callable(value):
|
|
3730
|
+
return value
|
|
3731
|
+
_validate_positive_generation(value)
|
|
3732
|
+
return lambda: value
|
|
3733
|
+
|
|
3734
|
+
|
|
3735
|
+
def _validate_positive_generation(value: object) -> None:
|
|
3736
|
+
if type(value) is not int or value <= 0:
|
|
3737
|
+
raise ValueError("worker generation fence must be positive")
|
|
3738
|
+
|
|
3739
|
+
|
|
3740
|
+
def _new_lease_id(leases: dict[str, _OperationLease]) -> str:
|
|
3741
|
+
while True:
|
|
3742
|
+
value = token_urlsafe(32)
|
|
3743
|
+
if value not in leases:
|
|
3744
|
+
return value
|
|
3745
|
+
|
|
3746
|
+
|
|
3747
|
+
def _generation_root_key(generation: int) -> str:
|
|
3748
|
+
_validate_positive_generation(generation)
|
|
3749
|
+
return f"generation-{generation}"
|
|
3750
|
+
|
|
3751
|
+
|
|
3752
|
+
def _bsl_identifier(value: object) -> bool:
|
|
3753
|
+
return (
|
|
3754
|
+
isinstance(value, str)
|
|
3755
|
+
and fullmatch(r"[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё_0-9]*", value) is not None
|
|
3756
|
+
and value.upper() not in _BSL_KEYWORDS
|
|
3757
|
+
)
|
|
3758
|
+
|
|
3759
|
+
|
|
3760
|
+
def _valid_dependency_target(value: object) -> bool:
|
|
3761
|
+
return (
|
|
3762
|
+
isinstance(value, DependencyTarget)
|
|
3763
|
+
and _bsl_identifier(value.source_module)
|
|
3764
|
+
and _bsl_identifier(value.export_variable)
|
|
3765
|
+
and value.target_kind in ("original", "overloaded")
|
|
3766
|
+
and _bsl_identifier(value.target_module)
|
|
3767
|
+
and not (
|
|
3768
|
+
value.target_kind == "original"
|
|
3769
|
+
and value.target_module.casefold()
|
|
3770
|
+
in _FIXED_UNQUALIFIED_WORKER_IDENTIFIERS
|
|
3771
|
+
)
|
|
3772
|
+
)
|