lsst-pipe-base 29.2025.4100__py3-none-any.whl → 29.2025.4300__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.
- lsst/pipe/base/_status.py +1 -1
- lsst/pipe/base/cli/cmd/__init__.py +2 -2
- lsst/pipe/base/cli/cmd/commands.py +116 -1
- lsst/pipe/base/graph_walker.py +8 -4
- lsst/pipe/base/pipeline_graph/_pipeline_graph.py +30 -5
- lsst/pipe/base/quantum_graph/__init__.py +1 -0
- lsst/pipe/base/quantum_graph/_common.py +2 -1
- lsst/pipe/base/quantum_graph/_multiblock.py +41 -7
- lsst/pipe/base/quantum_graph/_predicted.py +62 -5
- lsst/pipe/base/quantum_graph/_provenance.py +1209 -0
- lsst/pipe/base/quantum_graph/aggregator/__init__.py +143 -0
- lsst/pipe/base/quantum_graph/aggregator/_communicators.py +981 -0
- lsst/pipe/base/quantum_graph/aggregator/_config.py +139 -0
- lsst/pipe/base/quantum_graph/aggregator/_ingester.py +312 -0
- lsst/pipe/base/quantum_graph/aggregator/_progress.py +208 -0
- lsst/pipe/base/quantum_graph/aggregator/_scanner.py +371 -0
- lsst/pipe/base/quantum_graph/aggregator/_structs.py +167 -0
- lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +225 -0
- lsst/pipe/base/quantum_graph/aggregator/_writer.py +593 -0
- lsst/pipe/base/resource_usage.py +183 -0
- lsst/pipe/base/simple_pipeline_executor.py +4 -1
- lsst/pipe/base/tests/util.py +31 -0
- lsst/pipe/base/version.py +1 -1
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/METADATA +1 -1
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/RECORD +33 -22
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/WHEEL +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/entry_points.txt +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/LICENSE +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/bsd_license.txt +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/licenses/gpl-v3.0.txt +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/top_level.txt +0 -0
- {lsst_pipe_base-29.2025.4100.dist-info → lsst_pipe_base-29.2025.4300.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# This file is part of pipe_base.
|
|
2
|
+
#
|
|
3
|
+
# Developed for the LSST Data Management System.
|
|
4
|
+
# This product includes software developed by the LSST Project
|
|
5
|
+
# (http://www.lsst.org).
|
|
6
|
+
# See the COPYRIGHT file at the top-level directory of this distribution
|
|
7
|
+
# for details of code ownership.
|
|
8
|
+
#
|
|
9
|
+
# This software is dual licensed under the GNU General Public License and also
|
|
10
|
+
# under a 3-clause BSD license. Recipients may choose which of these licenses
|
|
11
|
+
# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
|
|
12
|
+
# respectively. If you choose the GPL option then the following text applies
|
|
13
|
+
# (but note that there is still no warranty even if you opt for BSD instead):
|
|
14
|
+
#
|
|
15
|
+
# This program is free software: you can redistribute it and/or modify
|
|
16
|
+
# it under the terms of the GNU General Public License as published by
|
|
17
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
18
|
+
# (at your option) any later version.
|
|
19
|
+
#
|
|
20
|
+
# This program is distributed in the hope that it will be useful,
|
|
21
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
22
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
23
|
+
# GNU General Public License for more details.
|
|
24
|
+
#
|
|
25
|
+
# You should have received a copy of the GNU General Public License
|
|
26
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
__all__ = ("Scanner",)
|
|
31
|
+
|
|
32
|
+
import dataclasses
|
|
33
|
+
import itertools
|
|
34
|
+
import uuid
|
|
35
|
+
|
|
36
|
+
import zstandard
|
|
37
|
+
|
|
38
|
+
from lsst.daf.butler import ButlerLogRecords, DatasetRef, QuantumBackedButler
|
|
39
|
+
from lsst.utils.iteration import ensure_iterable
|
|
40
|
+
|
|
41
|
+
from ... import automatic_connection_constants as acc
|
|
42
|
+
from ..._status import QuantumSuccessCaveats
|
|
43
|
+
from ..._task_metadata import TaskMetadata
|
|
44
|
+
from ...pipeline_graph import PipelineGraph, TaskImportMode
|
|
45
|
+
from ...quantum_provenance_graph import ExceptionInfo
|
|
46
|
+
from ...resource_usage import QuantumResourceUsage
|
|
47
|
+
from .._multiblock import Compressor
|
|
48
|
+
from .._predicted import (
|
|
49
|
+
PredictedDatasetModel,
|
|
50
|
+
PredictedQuantumDatasetsModel,
|
|
51
|
+
PredictedQuantumGraphReader,
|
|
52
|
+
)
|
|
53
|
+
from ._communicators import ScannerCommunicator
|
|
54
|
+
from ._structs import IngestRequest, ScanReport, ScanResult, ScanStatus
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclasses.dataclass
|
|
58
|
+
class Scanner:
|
|
59
|
+
"""A helper class for the provenance aggregator that reads metadata and log
|
|
60
|
+
files and scans for which outputs exist.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
predicted_path: str
|
|
64
|
+
"""Path to the predicted quantum graph."""
|
|
65
|
+
|
|
66
|
+
butler_path: str
|
|
67
|
+
"""Path or alias to the central butler repository."""
|
|
68
|
+
|
|
69
|
+
comms: ScannerCommunicator
|
|
70
|
+
"""Communicator object for this worker."""
|
|
71
|
+
|
|
72
|
+
reader: PredictedQuantumGraphReader = dataclasses.field(init=False)
|
|
73
|
+
"""Reader for the predicted quantum graph."""
|
|
74
|
+
|
|
75
|
+
qbb: QuantumBackedButler = dataclasses.field(init=False)
|
|
76
|
+
"""A quantum-backed butler used for log and metadata reads and existence
|
|
77
|
+
checks for other outputs (when necessary).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
compressor: Compressor | None = None
|
|
81
|
+
"""Object used to compress JSON blocks.
|
|
82
|
+
|
|
83
|
+
This is `None` until a compression dictionary is received from the writer
|
|
84
|
+
process.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
init_quanta: dict[uuid.UUID, PredictedQuantumDatasetsModel] = dataclasses.field(init=False)
|
|
88
|
+
"""Dictionary mapping init quantum IDs to their predicted models."""
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
if self.comms.config.mock_storage_classes:
|
|
92
|
+
import lsst.pipe.base.tests.mocks # noqa: F401
|
|
93
|
+
self.comms.log.verbose("Reading from predicted quantum graph.")
|
|
94
|
+
self.reader = self.comms.enter(
|
|
95
|
+
PredictedQuantumGraphReader.open(self.predicted_path, import_mode=TaskImportMode.DO_NOT_IMPORT)
|
|
96
|
+
)
|
|
97
|
+
self.reader.read_dimension_data()
|
|
98
|
+
self.reader.read_init_quanta()
|
|
99
|
+
self.comms.log.verbose("Initializing quantum-backed butler.")
|
|
100
|
+
self.qbb = self.make_qbb(self.butler_path, self.reader.pipeline_graph)
|
|
101
|
+
self.init_quanta = {q.quantum_id: q for q in self.reader.components.init_quanta.root}
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def make_qbb(butler_config: str, pipeline_graph: PipelineGraph) -> QuantumBackedButler:
|
|
105
|
+
"""Make quantum-backed butler that can operate on the outputs of the
|
|
106
|
+
quantum graph.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
butler_config : `str`
|
|
111
|
+
Path or alias for the central butler repository that shares storage
|
|
112
|
+
with the quantum-backed butler.
|
|
113
|
+
pipeline_graph : `..pipeline_graph.PipelineGraph`
|
|
114
|
+
Graph of tasks and dataset types.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
qbb : `lsst.daf.butler.QuantumBackedButler`
|
|
119
|
+
Quantum-backed butler. This does not have the datastore records
|
|
120
|
+
needed to read overall-inputs.
|
|
121
|
+
"""
|
|
122
|
+
return QuantumBackedButler.from_predicted(
|
|
123
|
+
butler_config,
|
|
124
|
+
predicted_inputs=[],
|
|
125
|
+
predicted_outputs=[],
|
|
126
|
+
dimensions=pipeline_graph.universe,
|
|
127
|
+
# We don't need the datastore records in the QG because we're
|
|
128
|
+
# only going to read metadata and logs, and those are never
|
|
129
|
+
# overall inputs.
|
|
130
|
+
datastore_records={},
|
|
131
|
+
dataset_types={node.name: node.dataset_type for node in pipeline_graph.dataset_types.values()},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def pipeline_graph(self) -> PipelineGraph:
|
|
136
|
+
"""Graph of tasks and dataset types."""
|
|
137
|
+
return self.reader.pipeline_graph
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def run(predicted_path: str, butler_path: str, comms: ScannerCommunicator) -> None:
|
|
141
|
+
"""Run the scanner.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
predicted_path : `str`
|
|
146
|
+
Path to the predicted quantum graph.
|
|
147
|
+
butler_path : `str`
|
|
148
|
+
Path or alias to the central butler repository.
|
|
149
|
+
comms : `ScannerCommunicator`
|
|
150
|
+
Communicator for the scanner.
|
|
151
|
+
|
|
152
|
+
Notes
|
|
153
|
+
-----
|
|
154
|
+
This method is designed to run as the ``target`` in
|
|
155
|
+
`WorkerContext.make_worker`.
|
|
156
|
+
"""
|
|
157
|
+
with comms:
|
|
158
|
+
scanner = Scanner(predicted_path, butler_path, comms)
|
|
159
|
+
scanner.loop()
|
|
160
|
+
|
|
161
|
+
def loop(self) -> None:
|
|
162
|
+
"""Run the main loop for the scanner."""
|
|
163
|
+
self.comms.log.info("Scan request loop beginning.")
|
|
164
|
+
for quantum_id in self.comms.poll():
|
|
165
|
+
if self.compressor is None and (cdict_data := self.comms.get_compression_dict()) is not None:
|
|
166
|
+
self.compressor = zstandard.ZstdCompressor(
|
|
167
|
+
self.comms.config.zstd_level, zstandard.ZstdCompressionDict(cdict_data)
|
|
168
|
+
)
|
|
169
|
+
self.scan_quantum(quantum_id)
|
|
170
|
+
|
|
171
|
+
def scan_dataset(self, predicted: PredictedDatasetModel) -> bool:
|
|
172
|
+
"""Scan for a dataset's existence.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
predicted : `.PredictedDatasetModel`
|
|
177
|
+
Information about the dataset from the predicted graph.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
exists : `bool``
|
|
182
|
+
Whether the dataset exists
|
|
183
|
+
"""
|
|
184
|
+
ref = self.reader.components.make_dataset_ref(predicted)
|
|
185
|
+
return self.qbb.stored(ref)
|
|
186
|
+
|
|
187
|
+
def scan_quantum(self, quantum_id: uuid.UUID) -> ScanResult:
|
|
188
|
+
"""Scan for a quantum's completion and error status, and its output
|
|
189
|
+
datasets' existence.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
quantum_id : `uuid.UUID`
|
|
194
|
+
Unique ID for the quantum.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
result : `ScanResult`
|
|
199
|
+
Scan result struct.
|
|
200
|
+
"""
|
|
201
|
+
if (predicted_quantum := self.init_quanta.get(quantum_id)) is not None:
|
|
202
|
+
result = ScanResult(predicted_quantum.quantum_id, status=ScanStatus.INIT)
|
|
203
|
+
self.comms.log.debug("Created init scan for %s (%s)", quantum_id, predicted_quantum.task_label)
|
|
204
|
+
else:
|
|
205
|
+
self.reader.read_quantum_datasets([quantum_id])
|
|
206
|
+
predicted_quantum = self.reader.components.quantum_datasets[quantum_id]
|
|
207
|
+
self.comms.log.debug(
|
|
208
|
+
"Scanning %s (%s@%s)",
|
|
209
|
+
quantum_id,
|
|
210
|
+
predicted_quantum.task_label,
|
|
211
|
+
predicted_quantum.data_coordinate,
|
|
212
|
+
)
|
|
213
|
+
result = ScanResult(predicted_quantum.quantum_id, ScanStatus.INCOMPLETE)
|
|
214
|
+
del self.reader.components.quantum_datasets[quantum_id]
|
|
215
|
+
log_id = self._read_and_compress_log(predicted_quantum, result)
|
|
216
|
+
if not self.comms.config.assume_complete and not result.log:
|
|
217
|
+
self.comms.log.debug("Abandoning scan for %s; no log dataset.", quantum_id)
|
|
218
|
+
result.status = ScanStatus.ABANDONED
|
|
219
|
+
self.comms.report_scan(ScanReport(result.quantum_id, result.status))
|
|
220
|
+
return result
|
|
221
|
+
metadata_id = self._read_and_compress_metadata(predicted_quantum, result)
|
|
222
|
+
if result.metadata:
|
|
223
|
+
result.status = ScanStatus.SUCCESSFUL
|
|
224
|
+
result.existing_outputs.add(metadata_id)
|
|
225
|
+
elif self.comms.config.assume_complete:
|
|
226
|
+
result.status = ScanStatus.FAILED
|
|
227
|
+
else:
|
|
228
|
+
# We found the log dataset, but no metadata; this means the
|
|
229
|
+
# quantum failed, but a retry might still happen that could
|
|
230
|
+
# turn it into a success if we can't yet assume the run is
|
|
231
|
+
# complete.
|
|
232
|
+
self.comms.log.debug("Abandoning scan for %s.", quantum_id)
|
|
233
|
+
result.status = ScanStatus.ABANDONED
|
|
234
|
+
self.comms.report_scan(ScanReport(result.quantum_id, result.status))
|
|
235
|
+
return result
|
|
236
|
+
if result.log:
|
|
237
|
+
result.existing_outputs.add(log_id)
|
|
238
|
+
for predicted_output in itertools.chain.from_iterable(predicted_quantum.outputs.values()):
|
|
239
|
+
if predicted_output.dataset_id not in result.existing_outputs and self.scan_dataset(
|
|
240
|
+
predicted_output
|
|
241
|
+
):
|
|
242
|
+
result.existing_outputs.add(predicted_output.dataset_id)
|
|
243
|
+
to_ingest = self._make_ingest_request(predicted_quantum, result)
|
|
244
|
+
self.comms.report_scan(ScanReport(result.quantum_id, result.status))
|
|
245
|
+
assert result.status is not ScanStatus.INCOMPLETE
|
|
246
|
+
assert result.status is not ScanStatus.ABANDONED
|
|
247
|
+
if self.comms.config.output_path is not None:
|
|
248
|
+
self.comms.request_write(result)
|
|
249
|
+
self.comms.request_ingest(to_ingest)
|
|
250
|
+
self.comms.log.debug("Finished scan for %s.", quantum_id)
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
def _make_ingest_request(
|
|
254
|
+
self, predicted_quantum: PredictedQuantumDatasetsModel, result: ScanResult
|
|
255
|
+
) -> IngestRequest:
|
|
256
|
+
"""Make an ingest request from a quantum scan.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
predicted_quantum : `PredictedQuantumDatasetsModel`
|
|
261
|
+
Information about the predicted quantum.
|
|
262
|
+
result : `ScanResult`
|
|
263
|
+
Result of a quantum scan.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
ingest_request : `IngestRequest`
|
|
268
|
+
A request to be sent to the ingester.
|
|
269
|
+
"""
|
|
270
|
+
predicted_outputs_by_id = {
|
|
271
|
+
d.dataset_id: d for d in itertools.chain.from_iterable(predicted_quantum.outputs.values())
|
|
272
|
+
}
|
|
273
|
+
to_ingest_predicted: list[PredictedDatasetModel] = []
|
|
274
|
+
to_ingest_refs: list[DatasetRef] = []
|
|
275
|
+
for dataset_id in result.existing_outputs:
|
|
276
|
+
predicted_output = predicted_outputs_by_id[dataset_id]
|
|
277
|
+
to_ingest_predicted.append(predicted_output)
|
|
278
|
+
to_ingest_refs.append(self.reader.components.make_dataset_ref(predicted_output))
|
|
279
|
+
to_ingest_records = self.qbb._datastore.export_predicted_records(to_ingest_refs)
|
|
280
|
+
return IngestRequest(result.quantum_id, to_ingest_predicted, to_ingest_records)
|
|
281
|
+
|
|
282
|
+
def _read_and_compress_metadata(
|
|
283
|
+
self, predicted_quantum: PredictedQuantumDatasetsModel, result: ScanResult
|
|
284
|
+
) -> uuid.UUID:
|
|
285
|
+
"""Attempt to read the metadata dataset for a quantum to extract
|
|
286
|
+
provenance information from it.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
predicted_quantum : `PredictedQuantumDatasetsModel`
|
|
291
|
+
Information about the predicted quantum.
|
|
292
|
+
result : `ScanResult`
|
|
293
|
+
Result object to be modified in-place.
|
|
294
|
+
|
|
295
|
+
Returns
|
|
296
|
+
-------
|
|
297
|
+
dataset_id : `uuid.UUID`
|
|
298
|
+
UUID of the metadata dataset.
|
|
299
|
+
"""
|
|
300
|
+
assert not result.metadata, "We shouldn't be scanning again if we already read the metadata."
|
|
301
|
+
(predicted_dataset,) = predicted_quantum.outputs[acc.METADATA_OUTPUT_CONNECTION_NAME]
|
|
302
|
+
ref = self.reader.components.make_dataset_ref(predicted_dataset)
|
|
303
|
+
try:
|
|
304
|
+
# This assumes QBB metadata writes are atomic, which should be the
|
|
305
|
+
# case. If it's not we'll probably get pydantic validation errors
|
|
306
|
+
# here.
|
|
307
|
+
content: TaskMetadata = self.qbb.get(ref, storageClass="TaskMetadata")
|
|
308
|
+
except FileNotFoundError:
|
|
309
|
+
if not self.comms.config.assume_complete:
|
|
310
|
+
return ref.id
|
|
311
|
+
else:
|
|
312
|
+
try:
|
|
313
|
+
# Int conversion guards against spurious conversion to
|
|
314
|
+
# float that can apparently sometimes happen in
|
|
315
|
+
# TaskMetadata.
|
|
316
|
+
result.caveats = QuantumSuccessCaveats(int(content["quantum"]["caveats"]))
|
|
317
|
+
except LookupError:
|
|
318
|
+
pass
|
|
319
|
+
try:
|
|
320
|
+
result.exception = ExceptionInfo._from_metadata(
|
|
321
|
+
content[predicted_quantum.task_label]["failure"]
|
|
322
|
+
)
|
|
323
|
+
except LookupError:
|
|
324
|
+
pass
|
|
325
|
+
try:
|
|
326
|
+
result.existing_outputs = {
|
|
327
|
+
uuid.UUID(id_str) for id_str in ensure_iterable(content["quantum"].getArray("outputs"))
|
|
328
|
+
}
|
|
329
|
+
except LookupError:
|
|
330
|
+
pass
|
|
331
|
+
result.resource_usage = QuantumResourceUsage.from_task_metadata(content)
|
|
332
|
+
result.metadata = content.model_dump_json().encode()
|
|
333
|
+
if self.compressor is not None:
|
|
334
|
+
result.metadata = self.compressor.compress(result.metadata)
|
|
335
|
+
result.is_compressed = True
|
|
336
|
+
return ref.id
|
|
337
|
+
|
|
338
|
+
def _read_and_compress_log(
|
|
339
|
+
self, predicted_quantum: PredictedQuantumDatasetsModel, result: ScanResult
|
|
340
|
+
) -> uuid.UUID:
|
|
341
|
+
"""Attempt to read the log dataset for a quantum to test for the
|
|
342
|
+
quantum's completion (the log is always written last) and aggregate
|
|
343
|
+
the log content in the provenance quantum graph.
|
|
344
|
+
|
|
345
|
+
Parameters
|
|
346
|
+
----------
|
|
347
|
+
predicted_quantum : `PredictedQuantumDatasetsModel`
|
|
348
|
+
Information about the predicted quantum.
|
|
349
|
+
result : `ScanResult`
|
|
350
|
+
Result object to be modified in-place.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
dataset_id : `uuid.UUID`
|
|
355
|
+
UUID of the log dataset.
|
|
356
|
+
"""
|
|
357
|
+
(predicted_dataset,) = predicted_quantum.outputs[acc.LOG_OUTPUT_CONNECTION_NAME]
|
|
358
|
+
ref = self.reader.components.make_dataset_ref(predicted_dataset)
|
|
359
|
+
try:
|
|
360
|
+
# This assumes QBB log writes are atomic, which should be the case.
|
|
361
|
+
# If it's not we'll probably get pydantic validation errors here.
|
|
362
|
+
content: ButlerLogRecords = self.qbb.get(ref)
|
|
363
|
+
except FileNotFoundError:
|
|
364
|
+
if not self.comms.config.assume_complete:
|
|
365
|
+
return ref.id
|
|
366
|
+
else:
|
|
367
|
+
result.log = content.model_dump_json().encode()
|
|
368
|
+
if self.compressor is not None:
|
|
369
|
+
result.log = self.compressor.compress(result.log)
|
|
370
|
+
result.is_compressed = True
|
|
371
|
+
return ref.id
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# This file is part of pipe_base.
|
|
2
|
+
#
|
|
3
|
+
# Developed for the LSST Data Management System.
|
|
4
|
+
# This product includes software developed by the LSST Project
|
|
5
|
+
# (http://www.lsst.org).
|
|
6
|
+
# See the COPYRIGHT file at the top-level directory of this distribution
|
|
7
|
+
# for details of code ownership.
|
|
8
|
+
#
|
|
9
|
+
# This software is dual licensed under the GNU General Public License and also
|
|
10
|
+
# under a 3-clause BSD license. Recipients may choose which of these licenses
|
|
11
|
+
# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
|
|
12
|
+
# respectively. If you choose the GPL option then the following text applies
|
|
13
|
+
# (but note that there is still no warranty even if you opt for BSD instead):
|
|
14
|
+
#
|
|
15
|
+
# This program is free software: you can redistribute it and/or modify
|
|
16
|
+
# it under the terms of the GNU General Public License as published by
|
|
17
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
18
|
+
# (at your option) any later version.
|
|
19
|
+
#
|
|
20
|
+
# This program is distributed in the hope that it will be useful,
|
|
21
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
22
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
23
|
+
# GNU General Public License for more details.
|
|
24
|
+
#
|
|
25
|
+
# You should have received a copy of the GNU General Public License
|
|
26
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
__all__ = (
|
|
31
|
+
"IngestRequest",
|
|
32
|
+
"ScanReport",
|
|
33
|
+
"ScanResult",
|
|
34
|
+
"ScanStatus",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
import dataclasses
|
|
38
|
+
import enum
|
|
39
|
+
import uuid
|
|
40
|
+
|
|
41
|
+
from lsst.daf.butler.datastore.record_data import DatastoreRecordData
|
|
42
|
+
|
|
43
|
+
from ..._status import QuantumSuccessCaveats
|
|
44
|
+
from ...quantum_provenance_graph import ExceptionInfo, QuantumRunStatus
|
|
45
|
+
from ...resource_usage import QuantumResourceUsage
|
|
46
|
+
from .._common import DatastoreName
|
|
47
|
+
from .._predicted import PredictedDatasetModel
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ScanStatus(enum.Enum):
|
|
51
|
+
"""Status enum for quantum scanning.
|
|
52
|
+
|
|
53
|
+
Note that this records the status for the *scanning* which is distinct
|
|
54
|
+
from the status of the quantum's execution.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
INCOMPLETE = enum.auto()
|
|
58
|
+
"""The quantum is not necessarily done running, and cannot be scanned
|
|
59
|
+
conclusively yet.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
ABANDONED = enum.auto()
|
|
63
|
+
"""The quantum's execution appears to have failed but we cannot rule out
|
|
64
|
+
the possibility that it could be recovered, but we've also waited long
|
|
65
|
+
enough (according to `ScannerTimeConfigDict.retry_timeout`) that it's time
|
|
66
|
+
to stop trying for now.
|
|
67
|
+
|
|
68
|
+
This state means a later run with `ScannerConfig.assume_complete` is
|
|
69
|
+
required.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
SUCCESSFUL = enum.auto()
|
|
73
|
+
"""The quantum was conclusively scanned and was executed successfully,
|
|
74
|
+
unblocking scans for downstream quanta.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
FAILED = enum.auto()
|
|
78
|
+
"""The quantum was conclusively scanned and failed execution, blocking
|
|
79
|
+
scans for downstream quanta.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
BLOCKED = enum.auto()
|
|
83
|
+
"""A quantum upstream of this one failed."""
|
|
84
|
+
|
|
85
|
+
INIT = enum.auto()
|
|
86
|
+
"""Init quanta need special handling, because they don't have logs and
|
|
87
|
+
metadata.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclasses.dataclass
|
|
92
|
+
class ScanReport:
|
|
93
|
+
"""Minimal information needed about a completed scan by the supervisor."""
|
|
94
|
+
|
|
95
|
+
quantum_id: uuid.UUID
|
|
96
|
+
"""Unique ID of the quantum."""
|
|
97
|
+
|
|
98
|
+
status: ScanStatus
|
|
99
|
+
"""Combined status of the scan and the execution of the quantum."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclasses.dataclass
|
|
103
|
+
class IngestRequest:
|
|
104
|
+
"""A request to ingest datasets produced by a single quantum."""
|
|
105
|
+
|
|
106
|
+
producer_id: uuid.UUID
|
|
107
|
+
"""ID of the quantum that produced these datasets."""
|
|
108
|
+
|
|
109
|
+
datasets: list[PredictedDatasetModel]
|
|
110
|
+
"""Registry information about the datasets."""
|
|
111
|
+
|
|
112
|
+
records: dict[DatastoreName, DatastoreRecordData]
|
|
113
|
+
"""Datastore information about the datasets."""
|
|
114
|
+
|
|
115
|
+
def __bool__(self) -> bool:
|
|
116
|
+
return bool(self.datasets or self.records)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclasses.dataclass
|
|
120
|
+
class ScanResult:
|
|
121
|
+
"""A struct that represents the result of scanning a quantum."""
|
|
122
|
+
|
|
123
|
+
quantum_id: uuid.UUID
|
|
124
|
+
"""Unique ID for the quantum."""
|
|
125
|
+
|
|
126
|
+
status: ScanStatus
|
|
127
|
+
"""Combined status for the scan and the execution of the quantum."""
|
|
128
|
+
|
|
129
|
+
caveats: QuantumSuccessCaveats | None = None
|
|
130
|
+
"""Flags indicating caveats on successful quanta."""
|
|
131
|
+
|
|
132
|
+
exception: ExceptionInfo | None = None
|
|
133
|
+
"""Information about an exception raised when the quantum was executing."""
|
|
134
|
+
|
|
135
|
+
resource_usage: QuantumResourceUsage | None = None
|
|
136
|
+
"""Resource usage information (timing, memory use) for this quantum."""
|
|
137
|
+
|
|
138
|
+
existing_outputs: set[uuid.UUID] = dataclasses.field(default_factory=set)
|
|
139
|
+
"""Unique IDs of the output datasets that were actually written."""
|
|
140
|
+
|
|
141
|
+
metadata: bytes = b""
|
|
142
|
+
"""Raw content of the metadata dataset."""
|
|
143
|
+
|
|
144
|
+
log: bytes = b""
|
|
145
|
+
"""Raw content of the log dataset."""
|
|
146
|
+
|
|
147
|
+
is_compressed: bool = False
|
|
148
|
+
"""Whether the `metadata` and `log` attributes are compressed."""
|
|
149
|
+
|
|
150
|
+
def get_run_status(self) -> QuantumRunStatus:
|
|
151
|
+
"""Translate the scan status and metadata/log presence into a run
|
|
152
|
+
status.
|
|
153
|
+
"""
|
|
154
|
+
if self.status is ScanStatus.BLOCKED:
|
|
155
|
+
return QuantumRunStatus.BLOCKED
|
|
156
|
+
if self.status is ScanStatus.INIT:
|
|
157
|
+
return QuantumRunStatus.SUCCESSFUL
|
|
158
|
+
if self.log:
|
|
159
|
+
if self.metadata:
|
|
160
|
+
return QuantumRunStatus.SUCCESSFUL
|
|
161
|
+
else:
|
|
162
|
+
return QuantumRunStatus.FAILED
|
|
163
|
+
else:
|
|
164
|
+
if self.metadata:
|
|
165
|
+
return QuantumRunStatus.LOGS_MISSING
|
|
166
|
+
else:
|
|
167
|
+
return QuantumRunStatus.METADATA_MISSING
|