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,516 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from base64 import b64encode
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from re import fullmatch
|
|
8
|
+
from typing import Literal
|
|
9
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from onec_runtime.errors import ProtocolError
|
|
13
|
+
from onec_runtime.experiment import bsl_string_literal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MAX_WORKER_STAGE_BATCH_SIZE = 10
|
|
17
|
+
MAX_WORKER_STAGE_OUTCOME_BYTES = 128 * 1024
|
|
18
|
+
MAX_WORKER_STAGE_URL_CODEPOINTS = 4096
|
|
19
|
+
MAX_WORKER_STAGE_DIAGNOSTIC_CODEPOINTS = 4096
|
|
20
|
+
WORKER_STAGE_SCHEMA = "onec-worker-stage-batch-receipt"
|
|
21
|
+
WORKER_STAGE_SCHEMA_VERSION = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_identifier(value: object) -> bool:
|
|
25
|
+
return (
|
|
26
|
+
isinstance(value, str)
|
|
27
|
+
and fullmatch(r"[A-Za-z_\u0400-\u04ff][A-Za-z0-9_\u0400-\u04ff]*", value)
|
|
28
|
+
is not None
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ascii_registration_name(value: object) -> bool:
|
|
33
|
+
return (
|
|
34
|
+
type(value) is str
|
|
35
|
+
and fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", value) is not None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _sha256(value: object) -> bool:
|
|
40
|
+
return type(value) is str and fullmatch(r"[0-9a-f]{64}", value) is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _temp_storage_session_id(value: object) -> str | None:
|
|
44
|
+
if (
|
|
45
|
+
type(value) is not str
|
|
46
|
+
or not value
|
|
47
|
+
or len(value) > MAX_WORKER_STAGE_URL_CODEPOINTS
|
|
48
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in value)
|
|
49
|
+
):
|
|
50
|
+
return None
|
|
51
|
+
split = urlsplit(value)
|
|
52
|
+
if (
|
|
53
|
+
split.scheme
|
|
54
|
+
or split.netloc
|
|
55
|
+
or split.fragment
|
|
56
|
+
or not split.path.startswith("e1cib/tempstorage/")
|
|
57
|
+
or split.path == "e1cib/tempstorage/"
|
|
58
|
+
):
|
|
59
|
+
return None
|
|
60
|
+
try:
|
|
61
|
+
query = parse_qsl(split.query, keep_blank_values=True, strict_parsing=True)
|
|
62
|
+
except ValueError:
|
|
63
|
+
return None
|
|
64
|
+
seance_ids = [item for name, item in query if name == "seanceId"]
|
|
65
|
+
return seance_ids[0] if len(seance_ids) == 1 and seance_ids[0] else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _qualified_temp_storage_url(value: object) -> bool:
|
|
69
|
+
return _temp_storage_session_id(value) is not None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
73
|
+
class WorkerStageEntry:
|
|
74
|
+
logical_name: str
|
|
75
|
+
artifact_sha256: str
|
|
76
|
+
registration_name: str
|
|
77
|
+
artifact_bytes: bytes
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if (
|
|
81
|
+
type(self.logical_name) is not str
|
|
82
|
+
or not _safe_identifier(self.logical_name)
|
|
83
|
+
or not _sha256(self.artifact_sha256)
|
|
84
|
+
or not _ascii_registration_name(self.registration_name)
|
|
85
|
+
or type(self.artifact_bytes) is not bytes
|
|
86
|
+
or not self.artifact_bytes
|
|
87
|
+
):
|
|
88
|
+
raise ValueError("worker stage entry is invalid")
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return (
|
|
92
|
+
"WorkerStageEntry("
|
|
93
|
+
f"logical_name={self.logical_name!r}, "
|
|
94
|
+
f"artifact_sha256={self.artifact_sha256!r}, payload=<redacted>)"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True, slots=True)
|
|
99
|
+
class WorkerStageBatch:
|
|
100
|
+
batch_index: int
|
|
101
|
+
entries: tuple[WorkerStageEntry, ...]
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
if (
|
|
105
|
+
type(self.batch_index) is not int
|
|
106
|
+
or self.batch_index < 0
|
|
107
|
+
or type(self.entries) is not tuple
|
|
108
|
+
or not self.entries
|
|
109
|
+
or len(self.entries) > MAX_WORKER_STAGE_BATCH_SIZE
|
|
110
|
+
or not all(type(entry) is WorkerStageEntry for entry in self.entries)
|
|
111
|
+
):
|
|
112
|
+
raise ValueError("worker stage batch is invalid")
|
|
113
|
+
registrations = tuple(entry.registration_name.casefold() for entry in self.entries)
|
|
114
|
+
if len(set(registrations)) != len(registrations):
|
|
115
|
+
raise ValueError("worker stage batch registrations are not unique")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_worker_stage_batches(
|
|
119
|
+
entries: tuple[WorkerStageEntry, ...],
|
|
120
|
+
) -> tuple[WorkerStageBatch, ...]:
|
|
121
|
+
if type(entries) is not tuple or not all(
|
|
122
|
+
type(entry) is WorkerStageEntry for entry in entries
|
|
123
|
+
):
|
|
124
|
+
raise ValueError("worker stage entries are invalid")
|
|
125
|
+
registrations = tuple(entry.registration_name.casefold() for entry in entries)
|
|
126
|
+
if len(set(registrations)) != len(registrations):
|
|
127
|
+
raise ValueError("worker stage registrations are not unique")
|
|
128
|
+
return tuple(
|
|
129
|
+
WorkerStageBatch(
|
|
130
|
+
index // MAX_WORKER_STAGE_BATCH_SIZE,
|
|
131
|
+
entries[index : index + MAX_WORKER_STAGE_BATCH_SIZE],
|
|
132
|
+
)
|
|
133
|
+
for index in range(0, len(entries), MAX_WORKER_STAGE_BATCH_SIZE)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _positive_index(value: object) -> bool:
|
|
138
|
+
return type(value) is int and value > 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _batch_count_is_valid(batch: WorkerStageBatch, batch_count: object) -> bool:
|
|
142
|
+
return _positive_index(batch_count) and batch.batch_index < batch_count
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def worker_stage_batch_digest(batch: WorkerStageBatch, batch_count: int) -> str:
|
|
146
|
+
if type(batch) is not WorkerStageBatch or not _batch_count_is_valid(
|
|
147
|
+
batch, batch_count
|
|
148
|
+
):
|
|
149
|
+
raise ValueError("worker stage batch coordinates are invalid")
|
|
150
|
+
identity = {
|
|
151
|
+
"batch_index": batch.batch_index,
|
|
152
|
+
"batch_count": batch_count,
|
|
153
|
+
"entries": [
|
|
154
|
+
{
|
|
155
|
+
"logical_name": entry.logical_name,
|
|
156
|
+
"registration_name": entry.registration_name,
|
|
157
|
+
"artifact_sha256": entry.artifact_sha256,
|
|
158
|
+
}
|
|
159
|
+
for entry in batch.entries
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
payload = json.dumps(
|
|
163
|
+
identity,
|
|
164
|
+
ensure_ascii=False,
|
|
165
|
+
sort_keys=True,
|
|
166
|
+
separators=(",", ":"),
|
|
167
|
+
).encode("utf-8")
|
|
168
|
+
return sha256(payload).hexdigest()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
172
|
+
class WorkerStageRegistrationReceipt:
|
|
173
|
+
registration_name: str
|
|
174
|
+
artifact_sha256: str
|
|
175
|
+
temp_storage_url: str
|
|
176
|
+
|
|
177
|
+
def __post_init__(self) -> None:
|
|
178
|
+
if (
|
|
179
|
+
not _ascii_registration_name(self.registration_name)
|
|
180
|
+
or not _sha256(self.artifact_sha256)
|
|
181
|
+
or not _qualified_temp_storage_url(self.temp_storage_url)
|
|
182
|
+
):
|
|
183
|
+
raise ValueError("worker stage registration receipt is invalid")
|
|
184
|
+
|
|
185
|
+
def __repr__(self) -> str:
|
|
186
|
+
return (
|
|
187
|
+
"WorkerStageRegistrationReceipt("
|
|
188
|
+
f"registration_name={self.registration_name!r}, "
|
|
189
|
+
f"artifact_sha256={self.artifact_sha256!r}, "
|
|
190
|
+
"temp_storage_url=<redacted>)"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
195
|
+
class WorkerStageFailure:
|
|
196
|
+
item_index: int
|
|
197
|
+
phase: Literal["decode", "upload", "connect", "registration-result"]
|
|
198
|
+
outcome: Literal["known_pre_swap", "registration_outcome_unknown"]
|
|
199
|
+
diagnostic: str
|
|
200
|
+
orphan_url: str | None
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
if (
|
|
204
|
+
type(self.item_index) is not int
|
|
205
|
+
or self.item_index < 0
|
|
206
|
+
or type(self.phase) is not str
|
|
207
|
+
or self.phase not in {"decode", "upload", "connect", "registration-result"}
|
|
208
|
+
or type(self.outcome) is not str
|
|
209
|
+
or self.outcome not in {
|
|
210
|
+
"known_pre_swap",
|
|
211
|
+
"registration_outcome_unknown",
|
|
212
|
+
}
|
|
213
|
+
or type(self.diagnostic) is not str
|
|
214
|
+
or len(self.diagnostic) > MAX_WORKER_STAGE_DIAGNOSTIC_CODEPOINTS
|
|
215
|
+
or (
|
|
216
|
+
self.orphan_url is not None
|
|
217
|
+
and not _qualified_temp_storage_url(self.orphan_url)
|
|
218
|
+
)
|
|
219
|
+
):
|
|
220
|
+
raise ValueError("worker stage failure is invalid")
|
|
221
|
+
expected_outcome = (
|
|
222
|
+
"known_pre_swap"
|
|
223
|
+
if self.phase in {"decode", "upload"}
|
|
224
|
+
else "registration_outcome_unknown"
|
|
225
|
+
)
|
|
226
|
+
if self.outcome != expected_outcome:
|
|
227
|
+
raise ValueError("worker stage failure outcome is invalid")
|
|
228
|
+
if self.phase in {"connect", "registration-result"} and self.orphan_url is None:
|
|
229
|
+
raise ValueError("worker stage failure orphan URL is missing")
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
return (
|
|
233
|
+
"WorkerStageFailure("
|
|
234
|
+
f"item_index={self.item_index!r}, phase={self.phase!r}, "
|
|
235
|
+
f"outcome={self.outcome!r}, diagnostic=<redacted>, "
|
|
236
|
+
"orphan_url=<redacted>)"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
241
|
+
class WorkerStageBatchOutcome:
|
|
242
|
+
transaction_id: UUID
|
|
243
|
+
batch_index: int
|
|
244
|
+
batch_count: int
|
|
245
|
+
batch_digest: str
|
|
246
|
+
status: Literal["succeeded", "failed"]
|
|
247
|
+
connected: tuple[WorkerStageRegistrationReceipt, ...]
|
|
248
|
+
failure: WorkerStageFailure | None
|
|
249
|
+
|
|
250
|
+
def __post_init__(self) -> None:
|
|
251
|
+
if (
|
|
252
|
+
type(self.transaction_id) is not UUID
|
|
253
|
+
or type(self.batch_index) is not int
|
|
254
|
+
or self.batch_index < 0
|
|
255
|
+
or not _positive_index(self.batch_count)
|
|
256
|
+
or self.batch_index >= self.batch_count
|
|
257
|
+
or not _sha256(self.batch_digest)
|
|
258
|
+
or type(self.status) is not str
|
|
259
|
+
or self.status not in {"succeeded", "failed"}
|
|
260
|
+
or type(self.connected) is not tuple
|
|
261
|
+
or len(self.connected) > MAX_WORKER_STAGE_BATCH_SIZE
|
|
262
|
+
or not all(
|
|
263
|
+
type(item) is WorkerStageRegistrationReceipt
|
|
264
|
+
for item in self.connected
|
|
265
|
+
)
|
|
266
|
+
or (
|
|
267
|
+
self.failure is not None
|
|
268
|
+
and type(self.failure) is not WorkerStageFailure
|
|
269
|
+
)
|
|
270
|
+
or (self.status == "succeeded") != (self.failure is None)
|
|
271
|
+
):
|
|
272
|
+
raise ValueError("worker stage batch outcome is invalid")
|
|
273
|
+
|
|
274
|
+
def __repr__(self) -> str:
|
|
275
|
+
return (
|
|
276
|
+
"WorkerStageBatchOutcome("
|
|
277
|
+
f"transaction_id={self.transaction_id!r}, "
|
|
278
|
+
f"batch_index={self.batch_index!r}, batch_count={self.batch_count!r}, "
|
|
279
|
+
f"batch_digest={self.batch_digest!r}, status={self.status!r}, "
|
|
280
|
+
f"connected={self.connected!r}, failure={self.failure!r})"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def stage_worker_batch_instruction(
|
|
285
|
+
batch: WorkerStageBatch,
|
|
286
|
+
*,
|
|
287
|
+
batch_count: int,
|
|
288
|
+
transaction_id: UUID,
|
|
289
|
+
) -> str:
|
|
290
|
+
"""Upload and connect a sealed batch and return one bounded JSON outcome."""
|
|
291
|
+
if (
|
|
292
|
+
type(batch) is not WorkerStageBatch
|
|
293
|
+
or not _batch_count_is_valid(batch, batch_count)
|
|
294
|
+
or type(transaction_id) is not UUID
|
|
295
|
+
):
|
|
296
|
+
raise ValueError("worker stage batch coordinates are invalid")
|
|
297
|
+
|
|
298
|
+
digest = worker_stage_batch_digest(batch, batch_count)
|
|
299
|
+
lines = [
|
|
300
|
+
"ПодключенныеWorker = Новый Массив;",
|
|
301
|
+
"ОтказWorker = Неопределено;",
|
|
302
|
+
]
|
|
303
|
+
for item_index, entry in enumerate(batch.entries):
|
|
304
|
+
suffix = str(item_index)
|
|
305
|
+
encoded = bsl_string_literal(b64encode(entry.artifact_bytes).decode("ascii"))
|
|
306
|
+
registration = bsl_string_literal(entry.registration_name)
|
|
307
|
+
artifact_digest = bsl_string_literal(entry.artifact_sha256)
|
|
308
|
+
lines.extend(
|
|
309
|
+
(
|
|
310
|
+
"Если ОтказWorker = Неопределено Тогда",
|
|
311
|
+
f' ФазаWorker{suffix} = "decode";',
|
|
312
|
+
f" АдресАртефактаWorker{suffix} = Неопределено;",
|
|
313
|
+
" Попытка",
|
|
314
|
+
f" ДанныеАртефактаWorker{suffix} = Base64Значение({encoded});",
|
|
315
|
+
f' ФазаWorker{suffix} = "upload";',
|
|
316
|
+
f" АдресАртефактаWorker{suffix} = Строка("
|
|
317
|
+
f"ПоместитьВоВременноеХранилище(ДанныеАртефактаWorker{suffix}));",
|
|
318
|
+
f' ФазаWorker{suffix} = "connect";',
|
|
319
|
+
f" ИмяАртефактаWorker{suffix} = ВнешниеОбработки.Подключить("
|
|
320
|
+
f"АдресАртефактаWorker{suffix}, {registration}, Ложь);",
|
|
321
|
+
f" Если ИмяАртефактаWorker{suffix} <> {registration} Тогда",
|
|
322
|
+
f' ФазаWorker{suffix} = "registration-result";',
|
|
323
|
+
f" ОтказWorker = Новый Структура("
|
|
324
|
+
'"item_index,phase,outcome,diagnostic,orphan_url", '
|
|
325
|
+
f'{item_index}, ФазаWorker{suffix}, "registration_outcome_unknown", '
|
|
326
|
+
'"registration identity mismatch", '
|
|
327
|
+
f"АдресАртефактаWorker{suffix});",
|
|
328
|
+
" Иначе",
|
|
329
|
+
" ПодключениеWorker = Новый Структура("
|
|
330
|
+
'"registration_name,artifact_sha256,temp_storage_url", '
|
|
331
|
+
f"{registration}, {artifact_digest}, АдресАртефактаWorker{suffix});",
|
|
332
|
+
" ПодключенныеWorker.Добавить(ПодключениеWorker);",
|
|
333
|
+
" КонецЕсли;",
|
|
334
|
+
" Исключение",
|
|
335
|
+
f" ИсходWorker{suffix} = ?(ФазаWorker{suffix} = \"decode\" "
|
|
336
|
+
f"ИЛИ ФазаWorker{suffix} = \"upload\", \"known_pre_swap\", "
|
|
337
|
+
'"registration_outcome_unknown");',
|
|
338
|
+
f" СиротскийАдресWorker{suffix} = ?("
|
|
339
|
+
f"АдресАртефактаWorker{suffix} = Неопределено, Ложь, "
|
|
340
|
+
f"АдресАртефактаWorker{suffix});",
|
|
341
|
+
" ОтказWorker = Новый Структура("
|
|
342
|
+
'"item_index,phase,outcome,diagnostic,orphan_url", '
|
|
343
|
+
f"{item_index}, ФазаWorker{suffix}, ИсходWorker{suffix}, "
|
|
344
|
+
"Лев(ОписаниеОшибки(), 4096), "
|
|
345
|
+
f"СиротскийАдресWorker{suffix});",
|
|
346
|
+
" КонецПопытки;",
|
|
347
|
+
"КонецЕсли;",
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
lines.extend(
|
|
352
|
+
(
|
|
353
|
+
'СтатусWorker = ?(ОтказWorker = Неопределено, "succeeded", "failed");',
|
|
354
|
+
"ДокументWorker = Новый Структура("
|
|
355
|
+
'"schema,schema_version,transaction_id,batch_index,batch_count,'
|
|
356
|
+
'batch_digest,status,connected,failure", '
|
|
357
|
+
f'{bsl_string_literal(WORKER_STAGE_SCHEMA)}, {WORKER_STAGE_SCHEMA_VERSION}, '
|
|
358
|
+
f'{bsl_string_literal(str(transaction_id))}, {batch.batch_index}, '
|
|
359
|
+
f'{batch_count}, {bsl_string_literal(digest)}, СтатусWorker, '
|
|
360
|
+
"ПодключенныеWorker, ?(ОтказWorker = Неопределено, Ложь, ОтказWorker));",
|
|
361
|
+
"ЗаписьWorker = Новый ЗаписьJSON;",
|
|
362
|
+
"ЗаписьWorker.УстановитьСтроку("
|
|
363
|
+
"Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет));",
|
|
364
|
+
"ЗаписатьJSON(ЗаписьWorker, ДокументWorker);",
|
|
365
|
+
"Результат = ЗаписьWorker.Закрыть();",
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
return "\n".join(lines)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
372
|
+
result: dict[str, object] = {}
|
|
373
|
+
for key, value in pairs:
|
|
374
|
+
if key in result:
|
|
375
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
376
|
+
result[key] = value
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _invalid_constant(_value: str) -> object:
|
|
381
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _exact_keys(value: object, expected: set[str]) -> dict[str, object]:
|
|
385
|
+
if type(value) is not dict or set(value) != expected:
|
|
386
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
387
|
+
return value
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def parse_worker_stage_batch_outcome(
|
|
391
|
+
payload: str,
|
|
392
|
+
batch: WorkerStageBatch,
|
|
393
|
+
*,
|
|
394
|
+
batch_count: int,
|
|
395
|
+
transaction_id: UUID,
|
|
396
|
+
) -> WorkerStageBatchOutcome:
|
|
397
|
+
"""Parse only the current JSON outcome correlated to one sealed batch."""
|
|
398
|
+
if (
|
|
399
|
+
type(batch) is not WorkerStageBatch
|
|
400
|
+
or not _batch_count_is_valid(batch, batch_count)
|
|
401
|
+
or type(transaction_id) is not UUID
|
|
402
|
+
):
|
|
403
|
+
raise ValueError("worker stage batch coordinates are invalid")
|
|
404
|
+
if type(payload) is not str:
|
|
405
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
406
|
+
try:
|
|
407
|
+
encoded = payload.encode("utf-8")
|
|
408
|
+
except UnicodeEncodeError as error:
|
|
409
|
+
raise ProtocolError("Worker stage outcome is invalid") from error
|
|
410
|
+
if len(encoded) > MAX_WORKER_STAGE_OUTCOME_BYTES:
|
|
411
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
412
|
+
try:
|
|
413
|
+
document = json.loads(
|
|
414
|
+
payload,
|
|
415
|
+
object_pairs_hook=_unique_object,
|
|
416
|
+
parse_constant=_invalid_constant,
|
|
417
|
+
)
|
|
418
|
+
except (json.JSONDecodeError, ProtocolError, RecursionError) as error:
|
|
419
|
+
raise ProtocolError("Worker stage outcome is invalid") from error
|
|
420
|
+
document = _exact_keys(
|
|
421
|
+
document,
|
|
422
|
+
{
|
|
423
|
+
"schema",
|
|
424
|
+
"schema_version",
|
|
425
|
+
"transaction_id",
|
|
426
|
+
"batch_index",
|
|
427
|
+
"batch_count",
|
|
428
|
+
"batch_digest",
|
|
429
|
+
"status",
|
|
430
|
+
"connected",
|
|
431
|
+
"failure",
|
|
432
|
+
},
|
|
433
|
+
)
|
|
434
|
+
digest = worker_stage_batch_digest(batch, batch_count)
|
|
435
|
+
if (
|
|
436
|
+
document["schema"] != WORKER_STAGE_SCHEMA
|
|
437
|
+
or type(document["schema_version"]) is not int
|
|
438
|
+
or document["schema_version"] != WORKER_STAGE_SCHEMA_VERSION
|
|
439
|
+
or document["transaction_id"] != str(transaction_id)
|
|
440
|
+
or type(document["batch_index"]) is not int
|
|
441
|
+
or document["batch_index"] != batch.batch_index
|
|
442
|
+
or type(document["batch_count"]) is not int
|
|
443
|
+
or document["batch_count"] != batch_count
|
|
444
|
+
or document["batch_digest"] != digest
|
|
445
|
+
or document["status"] not in {"succeeded", "failed"}
|
|
446
|
+
or type(document["connected"]) is not list
|
|
447
|
+
):
|
|
448
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
449
|
+
|
|
450
|
+
connected: list[WorkerStageRegistrationReceipt] = []
|
|
451
|
+
for index, raw in enumerate(document["connected"]):
|
|
452
|
+
if index >= len(batch.entries):
|
|
453
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
454
|
+
row = _exact_keys(
|
|
455
|
+
raw,
|
|
456
|
+
{"registration_name", "artifact_sha256", "temp_storage_url"},
|
|
457
|
+
)
|
|
458
|
+
entry = batch.entries[index]
|
|
459
|
+
if (
|
|
460
|
+
row["registration_name"] != entry.registration_name
|
|
461
|
+
or row["artifact_sha256"] != entry.artifact_sha256
|
|
462
|
+
):
|
|
463
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
464
|
+
try:
|
|
465
|
+
connected.append(
|
|
466
|
+
WorkerStageRegistrationReceipt(
|
|
467
|
+
entry.registration_name,
|
|
468
|
+
entry.artifact_sha256,
|
|
469
|
+
row["temp_storage_url"], # type: ignore[arg-type]
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
except ValueError as error:
|
|
473
|
+
raise ProtocolError("Worker stage outcome is invalid") from error
|
|
474
|
+
if len({item.temp_storage_url for item in connected}) != len(connected):
|
|
475
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
476
|
+
|
|
477
|
+
status = document["status"]
|
|
478
|
+
failure: WorkerStageFailure | None
|
|
479
|
+
if status == "succeeded":
|
|
480
|
+
if document["failure"] is not False or len(connected) != len(batch.entries):
|
|
481
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
482
|
+
failure = None
|
|
483
|
+
else:
|
|
484
|
+
raw_failure = _exact_keys(
|
|
485
|
+
document["failure"],
|
|
486
|
+
{"item_index", "phase", "outcome", "diagnostic", "orphan_url"},
|
|
487
|
+
)
|
|
488
|
+
if (
|
|
489
|
+
type(raw_failure["item_index"]) is not int
|
|
490
|
+
or raw_failure["item_index"] != len(connected)
|
|
491
|
+
or raw_failure["item_index"] >= len(batch.entries)
|
|
492
|
+
):
|
|
493
|
+
raise ProtocolError("Worker stage outcome is invalid")
|
|
494
|
+
try:
|
|
495
|
+
orphan_url = raw_failure["orphan_url"]
|
|
496
|
+
if orphan_url is False:
|
|
497
|
+
orphan_url = None
|
|
498
|
+
failure = WorkerStageFailure(
|
|
499
|
+
raw_failure["item_index"],
|
|
500
|
+
raw_failure["phase"], # type: ignore[arg-type]
|
|
501
|
+
raw_failure["outcome"], # type: ignore[arg-type]
|
|
502
|
+
raw_failure["diagnostic"], # type: ignore[arg-type]
|
|
503
|
+
orphan_url, # type: ignore[arg-type]
|
|
504
|
+
)
|
|
505
|
+
except ValueError as error:
|
|
506
|
+
raise ProtocolError("Worker stage outcome is invalid") from error
|
|
507
|
+
|
|
508
|
+
return WorkerStageBatchOutcome(
|
|
509
|
+
transaction_id,
|
|
510
|
+
batch.batch_index,
|
|
511
|
+
batch_count,
|
|
512
|
+
digest,
|
|
513
|
+
status, # type: ignore[arg-type]
|
|
514
|
+
tuple(connected),
|
|
515
|
+
failure,
|
|
516
|
+
)
|