lsst-pipe-base 30.0.0rc2__py3-none-any.whl → 30.0.0rc3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. lsst/pipe/base/_instrument.py +6 -5
  2. lsst/pipe/base/log_capture.py +39 -79
  3. lsst/pipe/base/log_on_close.py +79 -0
  4. lsst/pipe/base/mp_graph_executor.py +51 -15
  5. lsst/pipe/base/quantum_graph/_common.py +4 -3
  6. lsst/pipe/base/quantum_graph/_multiblock.py +6 -16
  7. lsst/pipe/base/quantum_graph/_predicted.py +106 -12
  8. lsst/pipe/base/quantum_graph/_provenance.py +657 -6
  9. lsst/pipe/base/quantum_graph/aggregator/_communicators.py +18 -50
  10. lsst/pipe/base/quantum_graph/aggregator/_scanner.py +35 -229
  11. lsst/pipe/base/quantum_graph/aggregator/_structs.py +3 -113
  12. lsst/pipe/base/quantum_graph/aggregator/_supervisor.py +10 -5
  13. lsst/pipe/base/quantum_graph/aggregator/_writer.py +31 -348
  14. lsst/pipe/base/quantum_graph/formatter.py +101 -0
  15. lsst/pipe/base/quantum_graph_builder.py +12 -1
  16. lsst/pipe/base/quantum_graph_executor.py +116 -13
  17. lsst/pipe/base/quantum_graph_skeleton.py +1 -7
  18. lsst/pipe/base/separable_pipeline_executor.py +18 -2
  19. lsst/pipe/base/single_quantum_executor.py +53 -35
  20. lsst/pipe/base/version.py +1 -1
  21. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/METADATA +1 -1
  22. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/RECORD +30 -28
  23. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/WHEEL +0 -0
  24. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/entry_points.txt +0 -0
  25. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/licenses/COPYRIGHT +0 -0
  26. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/licenses/LICENSE +0 -0
  27. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/licenses/bsd_license.txt +0 -0
  28. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/licenses/gpl-v3.0.txt +0 -0
  29. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/top_level.txt +0 -0
  30. {lsst_pipe_base-30.0.0rc2.dist-info → lsst_pipe_base-30.0.0rc3.dist-info}/zip-safe +0 -0
@@ -51,16 +51,17 @@ import time
51
51
  import uuid
52
52
  from abc import ABC, abstractmethod
53
53
  from collections.abc import Callable, Iterable, Iterator
54
- from contextlib import AbstractContextManager, ExitStack, contextmanager
54
+ from contextlib import ExitStack
55
55
  from traceback import format_exception
56
56
  from types import TracebackType
57
57
  from typing import Any, Literal, Self, TypeAlias, TypeVar, Union
58
58
 
59
- from lsst.utils.logging import VERBOSE, LsstLogAdapter
59
+ from lsst.utils.logging import LsstLogAdapter
60
60
 
61
+ from .._provenance import ProvenanceQuantumScanData
61
62
  from ._config import AggregatorConfig
62
63
  from ._progress import ProgressManager, make_worker_log
63
- from ._structs import IngestRequest, ScanReport, WriteRequest
64
+ from ._structs import IngestRequest, ScanReport
64
65
 
65
66
  _T = TypeVar("_T")
66
67
 
@@ -361,9 +362,9 @@ class SupervisorCommunicator:
361
362
  # scanner and the supervisor send one sentinal when done, and the
362
363
  # writer waits for (n_scanners + 1) sentinals to arrive before it
363
364
  # starts its shutdown.
364
- self._write_requests: Queue[WriteRequest | Literal[_Sentinel.NO_MORE_WRITE_REQUESTS]] | None = (
365
- context.make_queue() if config.output_path is not None else None
366
- )
365
+ self._write_requests: (
366
+ Queue[ProvenanceQuantumScanData | Literal[_Sentinel.NO_MORE_WRITE_REQUESTS]] | None
367
+ ) = context.make_queue() if config.output_path is not None else None
367
368
  # All other workers use this queue to send many different kinds of
368
369
  # reports the supervisor. The supervisor waits for a _DONE sentinal
369
370
  # from each worker before it finishes its shutdown.
@@ -461,12 +462,12 @@ class SupervisorCommunicator:
461
462
  """
462
463
  self._scan_requests.put(_ScanRequest(quantum_id), block=False)
463
464
 
464
- def request_write(self, request: WriteRequest) -> None:
465
+ def request_write(self, request: ProvenanceQuantumScanData) -> None:
465
466
  """Send a request to the writer to write provenance for the given scan.
466
467
 
467
468
  Parameters
468
469
  ----------
469
- request : `WriteRequest`
470
+ request : `ProvenanceQuantumScanData`
470
471
  Information from scanning a quantum (or knowing you don't have to,
471
472
  in the case of blocked quanta).
472
473
  """
@@ -621,6 +622,11 @@ class WorkerCommunicator:
621
622
  self._exit_stack.__exit__(exc_type, exc_value, traceback)
622
623
  return True
623
624
 
625
+ @property
626
+ def exit_stack(self) -> ExitStack:
627
+ """A `contextlib.ExitStack` tied to the communicator."""
628
+ return self._exit_stack
629
+
624
630
  def log_progress(self, level: int, message: str) -> None:
625
631
  """Send a high-level log message to the supervisor.
626
632
 
@@ -633,44 +639,6 @@ class WorkerCommunicator:
633
639
  """
634
640
  self._reports.put(_ProgressLog(message=message, level=level), block=False)
635
641
 
636
- def enter(
637
- self,
638
- cm: AbstractContextManager[_T],
639
- on_close: str | None = None,
640
- level: int = VERBOSE,
641
- is_progress_log: bool = False,
642
- ) -> _T:
643
- """Enter a context manager that will be exited when the communicator's
644
- context is exited.
645
-
646
- Parameters
647
- ----------
648
- cm : `contextlib.AbstractContextManager`
649
- A context manager to enter.
650
- on_close : `str`, optional
651
- A log message to emit (on the worker's logger) just before the
652
- given context manager is exited. This can be used to indicate
653
- what's going on when an ``__exit__`` implementation has a lot of
654
- work to do (e.g. moving a large file into a zip archive).
655
- level : `int`, optional
656
- Level for the ``on_close`` log message.
657
- is_progress_log : `bool`, optional
658
- If `True`, send the ``on_close`` message to the supervisor via
659
- `log_progress` as well as the worker's logger.
660
- """
661
- if on_close is None:
662
- return self._exit_stack.enter_context(cm)
663
-
664
- @contextmanager
665
- def wrapper() -> Iterator[_T]:
666
- with cm as result:
667
- yield result
668
- self.log.log(level, on_close)
669
- if is_progress_log:
670
- self.log_progress(level, on_close)
671
-
672
- return self._exit_stack.enter_context(wrapper())
673
-
674
642
  def check_for_cancel(self) -> None:
675
643
  """Check for a cancel signal from the supervisor and raise
676
644
  `FatalWorkerError` if it is present.
@@ -728,12 +696,12 @@ class ScannerCommunicator(WorkerCommunicator):
728
696
  else:
729
697
  self._reports.put(_IngestReport(1), block=False)
730
698
 
731
- def request_write(self, request: WriteRequest) -> None:
699
+ def request_write(self, request: ProvenanceQuantumScanData) -> None:
732
700
  """Ask the writer to write provenance for a quantum.
733
701
 
734
702
  Parameters
735
703
  ----------
736
- request : `WriteRequest`
704
+ request : `ProvenanceQuantumScanData`
737
705
  Result of scanning a quantum.
738
706
  """
739
707
  assert self._write_requests is not None, "Writer should not be used if writing is disabled."
@@ -913,12 +881,12 @@ class WriterCommunicator(WorkerCommunicator):
913
881
  self._reports.put(_Sentinel.WRITER_DONE, block=False)
914
882
  return result
915
883
 
916
- def poll(self) -> Iterator[WriteRequest]:
884
+ def poll(self) -> Iterator[ProvenanceQuantumScanData]:
917
885
  """Poll for writer requests from the scanner workers and supervisor.
918
886
 
919
887
  Yields
920
888
  ------
921
- request : `WriteRequest`
889
+ request : `ProvenanceQuantumScanData`
922
890
  The result of a quantum scan.
923
891
 
924
892
  Notes
@@ -38,23 +38,19 @@ from typing import Any, Literal, Self
38
38
  import zstandard
39
39
 
40
40
  from lsst.daf.butler import ButlerLogRecords, DatasetRef, QuantumBackedButler
41
- from lsst.utils.iteration import ensure_iterable
42
41
 
43
42
  from ... import automatic_connection_constants as acc
44
- from ..._status import ExceptionInfo, QuantumAttemptStatus, QuantumSuccessCaveats
45
43
  from ..._task_metadata import TaskMetadata
46
- from ...log_capture import _ExecutionLogRecordsExtra
47
44
  from ...pipeline_graph import PipelineGraph, TaskImportMode
48
- from ...resource_usage import QuantumResourceUsage
49
45
  from .._multiblock import Compressor
50
46
  from .._predicted import (
51
47
  PredictedDatasetModel,
52
48
  PredictedQuantumDatasetsModel,
53
49
  PredictedQuantumGraphReader,
54
50
  )
55
- from .._provenance import ProvenanceInitQuantumModel, ProvenanceQuantumAttemptModel, ProvenanceQuantumModel
51
+ from .._provenance import ProvenanceQuantumScanModels, ProvenanceQuantumScanStatus
56
52
  from ._communicators import ScannerCommunicator
57
- from ._structs import IngestRequest, InProgressScan, ScanReport, ScanStatus, WriteRequest
53
+ from ._structs import IngestRequest, ScanReport
58
54
 
59
55
 
60
56
  @dataclasses.dataclass
@@ -94,7 +90,7 @@ class Scanner(AbstractContextManager):
94
90
  if self.comms.config.mock_storage_classes:
95
91
  import lsst.pipe.base.tests.mocks # noqa: F401
96
92
  self.comms.log.verbose("Reading from predicted quantum graph.")
97
- self.reader = self.comms.enter(
93
+ self.reader = self.comms.exit_stack.enter_context(
98
94
  PredictedQuantumGraphReader.open(self.predicted_path, import_mode=TaskImportMode.DO_NOT_IMPORT)
99
95
  )
100
96
  self.reader.read_dimension_data()
@@ -196,7 +192,7 @@ class Scanner(AbstractContextManager):
196
192
  ref = self.reader.components.make_dataset_ref(predicted)
197
193
  return self.qbb.stored(ref)
198
194
 
199
- def scan_quantum(self, quantum_id: uuid.UUID) -> InProgressScan:
195
+ def scan_quantum(self, quantum_id: uuid.UUID) -> ProvenanceQuantumScanModels:
200
196
  """Scan for a quantum's completion and error status, and its output
201
197
  datasets' existence.
202
198
 
@@ -207,76 +203,38 @@ class Scanner(AbstractContextManager):
207
203
 
208
204
  Returns
209
205
  -------
210
- result : `InProgressScan`
206
+ result : `ProvenanceQuantumScanModels`
211
207
  Scan result struct.
212
208
  """
213
209
  if (predicted_quantum := self.init_quanta.get(quantum_id)) is not None:
214
- result = InProgressScan(predicted_quantum.quantum_id, status=ScanStatus.INIT)
210
+ result = ProvenanceQuantumScanModels(
211
+ predicted_quantum.quantum_id, status=ProvenanceQuantumScanStatus.INIT
212
+ )
215
213
  self.comms.log.debug("Created init scan for %s (%s)", quantum_id, predicted_quantum.task_label)
216
214
  else:
217
215
  self.reader.read_quantum_datasets([quantum_id])
218
- predicted_quantum = self.reader.components.quantum_datasets[quantum_id]
216
+ predicted_quantum = self.reader.components.quantum_datasets.pop(quantum_id)
219
217
  self.comms.log.debug(
220
218
  "Scanning %s (%s@%s)",
221
219
  quantum_id,
222
220
  predicted_quantum.task_label,
223
221
  predicted_quantum.data_coordinate,
224
222
  )
225
- result = InProgressScan(predicted_quantum.quantum_id, ScanStatus.INCOMPLETE)
226
- del self.reader.components.quantum_datasets[quantum_id]
227
- last_attempt = ProvenanceQuantumAttemptModel()
228
- if not self._read_log(predicted_quantum, result, last_attempt):
229
- self.comms.log.debug("Abandoning scan for %s; no log dataset.", quantum_id)
230
- self.comms.report_scan(ScanReport(result.quantum_id, result.status))
231
- return result
232
- if not self._read_metadata(predicted_quantum, result, last_attempt):
233
- # We found the log dataset, but no metadata; this means the
234
- # quantum failed, but a retry might still happen that could
235
- # turn it into a success if we can't yet assume the run is
236
- # complete.
237
- self.comms.log.debug("Abandoning scan for %s.", quantum_id)
223
+ logs = self._read_log(predicted_quantum)
224
+ metadata = self._read_metadata(predicted_quantum)
225
+ result = ProvenanceQuantumScanModels.from_metadata_and_logs(
226
+ predicted_quantum, metadata, logs, assume_complete=self.comms.config.assume_complete
227
+ )
228
+ if result.status is ProvenanceQuantumScanStatus.ABANDONED:
229
+ self.comms.log.debug("Abandoning scan for failed quantum %s.", quantum_id)
238
230
  self.comms.report_scan(ScanReport(result.quantum_id, result.status))
239
231
  return result
240
- last_attempt.attempt = len(result.attempts)
241
- result.attempts.append(last_attempt)
242
- assert result.status is not ScanStatus.INCOMPLETE
243
- assert result.status is not ScanStatus.ABANDONED
244
-
245
- if len(result.logs.attempts) < len(result.attempts):
246
- # Logs were not found for this attempt; must have been a hard error
247
- # that kept the `finally` block from running or otherwise
248
- # interrupted the writing of the logs.
249
- result.logs.attempts.append(None)
250
- if result.status is ScanStatus.SUCCESSFUL:
251
- # But we found the metadata! Either that hard error happened
252
- # at a very unlucky time (in between those two writes), or
253
- # something even weirder happened.
254
- result.attempts[-1].status = QuantumAttemptStatus.LOGS_MISSING
255
- else:
256
- result.attempts[-1].status = QuantumAttemptStatus.FAILED
257
- if len(result.metadata.attempts) < len(result.attempts):
258
- # Metadata missing usually just means a failure. In any case, the
259
- # status will already be correct, either because it was set to a
260
- # failure when we read the logs, or left at UNKNOWN if there were
261
- # no logs. Note that scanners never process BLOCKED quanta at all.
262
- result.metadata.attempts.append(None)
263
- assert len(result.logs.attempts) == len(result.attempts) or len(result.metadata.attempts) == len(
264
- result.attempts
265
- ), (
266
- "The only way we can add more than one quantum attempt is by "
267
- "extracting info stored with the logs, and that always appends "
268
- "a log attempt and a metadata attempt, so this must be a bug in "
269
- "the scanner."
270
- )
271
- # Scan for output dataset existence, skipping any the metadata reported
272
- # on as well as and the metadata and logs themselves (since we just
273
- # checked those).
274
232
  for predicted_output in itertools.chain.from_iterable(predicted_quantum.outputs.values()):
275
- if predicted_output.dataset_id not in result.outputs:
276
- result.outputs[predicted_output.dataset_id] = self.scan_dataset(predicted_output)
233
+ if predicted_output.dataset_id not in result.output_existence:
234
+ result.output_existence[predicted_output.dataset_id] = self.scan_dataset(predicted_output)
277
235
  to_ingest = self._make_ingest_request(predicted_quantum, result)
278
236
  if self.comms.config.output_path is not None:
279
- to_write = self._make_write_request(predicted_quantum, result)
237
+ to_write = result.to_scan_data(predicted_quantum, compressor=self.compressor)
280
238
  self.comms.request_write(to_write)
281
239
  self.comms.request_ingest(to_ingest)
282
240
  self.comms.report_scan(ScanReport(result.quantum_id, result.status))
@@ -284,7 +242,7 @@ class Scanner(AbstractContextManager):
284
242
  return result
285
243
 
286
244
  def _make_ingest_request(
287
- self, predicted_quantum: PredictedQuantumDatasetsModel, result: InProgressScan
245
+ self, predicted_quantum: PredictedQuantumDatasetsModel, result: ProvenanceQuantumScanModels
288
246
  ) -> IngestRequest:
289
247
  """Make an ingest request from a quantum scan.
290
248
 
@@ -292,7 +250,7 @@ class Scanner(AbstractContextManager):
292
250
  ----------
293
251
  predicted_quantum : `PredictedQuantumDatasetsModel`
294
252
  Information about the predicted quantum.
295
- result : `InProgressScan`
253
+ result : `ProvenanceQuantumScanModels`
296
254
  Result of a quantum scan.
297
255
 
298
256
  Returns
@@ -305,7 +263,7 @@ class Scanner(AbstractContextManager):
305
263
  }
306
264
  to_ingest_predicted: list[PredictedDatasetModel] = []
307
265
  to_ingest_refs: list[DatasetRef] = []
308
- for dataset_id, was_produced in result.outputs.items():
266
+ for dataset_id, was_produced in result.output_existence.items():
309
267
  if was_produced:
310
268
  predicted_output = predicted_outputs_by_id[dataset_id]
311
269
  to_ingest_predicted.append(predicted_output)
@@ -313,69 +271,18 @@ class Scanner(AbstractContextManager):
313
271
  to_ingest_records = self.qbb._datastore.export_predicted_records(to_ingest_refs)
314
272
  return IngestRequest(result.quantum_id, to_ingest_predicted, to_ingest_records)
315
273
 
316
- def _make_write_request(
317
- self, predicted_quantum: PredictedQuantumDatasetsModel, result: InProgressScan
318
- ) -> WriteRequest:
319
- """Make a write request from a quantum scan.
274
+ def _read_metadata(self, predicted_quantum: PredictedQuantumDatasetsModel) -> TaskMetadata | None:
275
+ """Attempt to read the metadata dataset for a quantum.
320
276
 
321
277
  Parameters
322
278
  ----------
323
279
  predicted_quantum : `PredictedQuantumDatasetsModel`
324
280
  Information about the predicted quantum.
325
- result : `InProgressScan`
326
- Result of a quantum scan.
327
281
 
328
282
  Returns
329
283
  -------
330
- write_request : `WriteRequest`
331
- A request to be sent to the writer.
332
- """
333
- quantum: ProvenanceInitQuantumModel | ProvenanceQuantumModel
334
- if result.status is ScanStatus.INIT:
335
- quantum = ProvenanceInitQuantumModel.from_predicted(predicted_quantum)
336
- else:
337
- quantum = ProvenanceQuantumModel.from_predicted(predicted_quantum)
338
- quantum.attempts = result.attempts
339
- request = WriteRequest(
340
- result.quantum_id,
341
- result.status,
342
- existing_outputs={
343
- dataset_id for dataset_id, was_produced in result.outputs.items() if was_produced
344
- },
345
- quantum=quantum.model_dump_json().encode(),
346
- logs=result.logs.model_dump_json().encode() if result.logs.attempts else b"",
347
- metadata=result.metadata.model_dump_json().encode() if result.metadata.attempts else b"",
348
- )
349
- if self.compressor is not None:
350
- request.quantum = self.compressor.compress(request.quantum)
351
- request.logs = self.compressor.compress(request.logs) if request.logs else b""
352
- request.metadata = self.compressor.compress(request.metadata) if request.metadata else b""
353
- request.is_compressed = True
354
- return request
355
-
356
- def _read_metadata(
357
- self,
358
- predicted_quantum: PredictedQuantumDatasetsModel,
359
- result: InProgressScan,
360
- last_attempt: ProvenanceQuantumAttemptModel,
361
- ) -> bool:
362
- """Attempt to read the metadata dataset for a quantum to extract
363
- provenance information from it.
364
-
365
- Parameters
366
- ----------
367
- predicted_quantum : `PredictedQuantumDatasetsModel`
368
- Information about the predicted quantum.
369
- result : `InProgressScan`
370
- Result object to be modified in-place.
371
- last_attempt : `ScanningProvenanceQuantumAttemptModel`
372
- Structure to fill in with information about the last attempt to
373
- run this quantum.
374
-
375
- Returns
376
- -------
377
- complete : `bool`
378
- Whether the quantum is complete.
284
+ metadata : `...TaskMetadata` or `None`
285
+ Task metadata.
379
286
  """
380
287
  (predicted_dataset,) = predicted_quantum.outputs[acc.METADATA_OUTPUT_CONNECTION_NAME]
381
288
  ref = self.reader.components.make_dataset_ref(predicted_dataset)
@@ -383,129 +290,28 @@ class Scanner(AbstractContextManager):
383
290
  # This assumes QBB metadata writes are atomic, which should be the
384
291
  # case. If it's not we'll probably get pydantic validation errors
385
292
  # here.
386
- metadata: TaskMetadata = self.qbb.get(ref, storageClass="TaskMetadata")
293
+ return self.qbb.get(ref, storageClass="TaskMetadata")
387
294
  except FileNotFoundError:
388
- result.outputs[ref.id] = False
389
- if self.comms.config.assume_complete:
390
- result.status = ScanStatus.FAILED
391
- else:
392
- result.status = ScanStatus.ABANDONED
393
- return False
394
- else:
395
- result.status = ScanStatus.SUCCESSFUL
396
- result.outputs[ref.id] = True
397
- last_attempt.status = QuantumAttemptStatus.SUCCESSFUL
398
- try:
399
- # Int conversion guards against spurious conversion to
400
- # float that can apparently sometimes happen in
401
- # TaskMetadata.
402
- last_attempt.caveats = QuantumSuccessCaveats(int(metadata["quantum"]["caveats"]))
403
- except LookupError:
404
- pass
405
- try:
406
- last_attempt.exception = ExceptionInfo._from_metadata(
407
- metadata[predicted_quantum.task_label]["failure"]
408
- )
409
- except LookupError:
410
- pass
411
- try:
412
- for id_str in ensure_iterable(metadata["quantum"].getArray("outputs")):
413
- result.outputs[uuid.UUID(id_str)]
414
- except LookupError:
415
- pass
416
- else:
417
- # If the metadata told us what it wrote, anything not in that
418
- # list was not written.
419
- for predicted_output in itertools.chain.from_iterable(predicted_quantum.outputs.values()):
420
- result.outputs.setdefault(predicted_output.dataset_id, False)
421
- last_attempt.resource_usage = QuantumResourceUsage.from_task_metadata(metadata)
422
- result.metadata.attempts.append(metadata)
423
- return True
424
-
425
- def _read_log(
426
- self,
427
- predicted_quantum: PredictedQuantumDatasetsModel,
428
- result: InProgressScan,
429
- last_attempt: ProvenanceQuantumAttemptModel,
430
- ) -> bool:
431
- """Attempt to read the log dataset for a quantum to test for the
432
- quantum's completion (the log is always written last) and aggregate
433
- the log content in the provenance quantum graph.
295
+ return None
296
+
297
+ def _read_log(self, predicted_quantum: PredictedQuantumDatasetsModel) -> ButlerLogRecords | None:
298
+ """Attempt to read the log dataset for a quantum.
434
299
 
435
300
  Parameters
436
301
  ----------
437
302
  predicted_quantum : `PredictedQuantumDatasetsModel`
438
303
  Information about the predicted quantum.
439
- result : `InProgressScan`
440
- Result object to be modified in-place.
441
- last_attempt : `ScanningProvenanceQuantumAttemptModel`
442
- Structure to fill in with information about the last attempt to
443
- run this quantum.
444
304
 
445
305
  Returns
446
306
  -------
447
- complete : `bool`
448
- Whether the quantum is complete.
307
+ logs : `lsst.daf.butler.logging.ButlerLogRecords` or `None`
308
+ Task logs.
449
309
  """
450
310
  (predicted_dataset,) = predicted_quantum.outputs[acc.LOG_OUTPUT_CONNECTION_NAME]
451
311
  ref = self.reader.components.make_dataset_ref(predicted_dataset)
452
312
  try:
453
313
  # This assumes QBB log writes are atomic, which should be the case.
454
314
  # If it's not we'll probably get pydantic validation errors here.
455
- log_records: ButlerLogRecords = self.qbb.get(ref)
315
+ return self.qbb.get(ref)
456
316
  except FileNotFoundError:
457
- result.outputs[ref.id] = False
458
- if self.comms.config.assume_complete:
459
- result.status = ScanStatus.FAILED
460
- else:
461
- result.status = ScanStatus.ABANDONED
462
- return False
463
- else:
464
- # Set the attempt's run status to FAILED, since the default is
465
- # UNKNOWN (i.e. logs *and* metadata are missing) and we now know
466
- # the logs exist. This will usually get replaced by SUCCESSFUL
467
- # when we look for metadata next.
468
- last_attempt.status = QuantumAttemptStatus.FAILED
469
- result.outputs[ref.id] = True
470
- if log_records.extra:
471
- log_extra = _ExecutionLogRecordsExtra.model_validate(log_records.extra)
472
- self._extract_from_log_extra(log_extra, result, last_attempt=last_attempt)
473
- result.logs.attempts.append(list(log_records))
474
- return True
475
-
476
- def _extract_from_log_extra(
477
- self,
478
- log_extra: _ExecutionLogRecordsExtra,
479
- result: InProgressScan,
480
- last_attempt: ProvenanceQuantumAttemptModel | None,
481
- ) -> None:
482
- for previous_attempt_log_extra in log_extra.previous_attempts:
483
- self._extract_from_log_extra(previous_attempt_log_extra, result, last_attempt=None)
484
- quantum_attempt: ProvenanceQuantumAttemptModel
485
- if last_attempt is None:
486
- # This is not the last attempt, so it must be a failure.
487
- quantum_attempt = ProvenanceQuantumAttemptModel(
488
- attempt=len(result.attempts), status=QuantumAttemptStatus.FAILED
489
- )
490
- # We also need to get the logs from this extra provenance, since
491
- # they won't be the main section of the log records.
492
- result.logs.attempts.append(log_extra.logs)
493
- # The special last attempt is only appended after we attempt to
494
- # read metadata later, but we have to append this one now.
495
- result.attempts.append(quantum_attempt)
496
- else:
497
- assert not log_extra.logs, "Logs for the last attempt should not be stored in the extra JSON."
498
- quantum_attempt = last_attempt
499
- if log_extra.exception is not None or log_extra.metadata is not None or last_attempt is None:
500
- # We won't be getting a separate metadata dataset, so anything we
501
- # might get from the metadata has to come from this extra
502
- # provenance in the logs.
503
- quantum_attempt.exception = log_extra.exception
504
- if log_extra.metadata is not None:
505
- quantum_attempt.resource_usage = QuantumResourceUsage.from_task_metadata(log_extra.metadata)
506
- result.metadata.attempts.append(log_extra.metadata)
507
- else:
508
- result.metadata.attempts.append(None)
509
- # Regardless of whether this is the last attempt or not, we can only
510
- # get the previous_process_quanta from the log extra.
511
- quantum_attempt.previous_process_quanta.extend(log_extra.previous_process_quanta)
317
+ return None
@@ -27,68 +27,16 @@
27
27
 
28
28
  from __future__ import annotations
29
29
 
30
- __all__ = (
31
- "InProgressScan",
32
- "IngestRequest",
33
- "ScanReport",
34
- "ScanStatus",
35
- "WriteRequest",
36
- )
30
+ __all__ = ("IngestRequest", "ScanReport")
37
31
 
38
32
  import dataclasses
39
- import enum
40
33
  import uuid
41
34
 
42
35
  from lsst.daf.butler.datastore.record_data import DatastoreRecordData
43
36
 
44
37
  from .._common import DatastoreName
45
38
  from .._predicted import PredictedDatasetModel
46
- from .._provenance import (
47
- ProvenanceLogRecordsModel,
48
- ProvenanceQuantumAttemptModel,
49
- ProvenanceTaskMetadataModel,
50
- )
51
-
52
-
53
- class ScanStatus(enum.Enum):
54
- """Status enum for quantum scanning.
55
-
56
- Note that this records the status for the *scanning* which is distinct
57
- from the status of the quantum's execution.
58
- """
59
-
60
- INCOMPLETE = enum.auto()
61
- """The quantum is not necessarily done running, and cannot be scanned
62
- conclusively yet.
63
- """
64
-
65
- ABANDONED = enum.auto()
66
- """The quantum's execution appears to have failed but we cannot rule out
67
- the possibility that it could be recovered, but we've also waited long
68
- enough (according to `ScannerTimeConfigDict.retry_timeout`) that it's time
69
- to stop trying for now.
70
-
71
- This state means a later run with `ScannerConfig.assume_complete` is
72
- required.
73
- """
74
-
75
- SUCCESSFUL = enum.auto()
76
- """The quantum was conclusively scanned and was executed successfully,
77
- unblocking scans for downstream quanta.
78
- """
79
-
80
- FAILED = enum.auto()
81
- """The quantum was conclusively scanned and failed execution, blocking
82
- scans for downstream quanta.
83
- """
84
-
85
- BLOCKED = enum.auto()
86
- """A quantum upstream of this one failed."""
87
-
88
- INIT = enum.auto()
89
- """Init quanta need special handling, because they don't have logs and
90
- metadata.
91
- """
39
+ from .._provenance import ProvenanceQuantumScanStatus
92
40
 
93
41
 
94
42
  @dataclasses.dataclass
@@ -98,7 +46,7 @@ class ScanReport:
98
46
  quantum_id: uuid.UUID
99
47
  """Unique ID of the quantum."""
100
48
 
101
- status: ScanStatus
49
+ status: ProvenanceQuantumScanStatus
102
50
  """Combined status of the scan and the execution of the quantum."""
103
51
 
104
52
 
@@ -117,61 +65,3 @@ class IngestRequest:
117
65
 
118
66
  def __bool__(self) -> bool:
119
67
  return bool(self.datasets or self.records)
120
-
121
-
122
- @dataclasses.dataclass
123
- class InProgressScan:
124
- """A struct that represents a quantum that is being scanned."""
125
-
126
- quantum_id: uuid.UUID
127
- """Unique ID for the quantum."""
128
-
129
- status: ScanStatus
130
- """Combined status for the scan and the execution of the quantum."""
131
-
132
- attempts: list[ProvenanceQuantumAttemptModel] = dataclasses.field(default_factory=list)
133
- """Provenance information about each attempt to run the quantum."""
134
-
135
- outputs: dict[uuid.UUID, bool] = dataclasses.field(default_factory=dict)
136
- """Unique IDs of the output datasets mapped to whether they were actually
137
- produced.
138
- """
139
-
140
- metadata: ProvenanceTaskMetadataModel = dataclasses.field(default_factory=ProvenanceTaskMetadataModel)
141
- """Task metadata information for each attempt.
142
- """
143
-
144
- logs: ProvenanceLogRecordsModel = dataclasses.field(default_factory=ProvenanceLogRecordsModel)
145
- """Log records for each attempt.
146
- """
147
-
148
-
149
- @dataclasses.dataclass
150
- class WriteRequest:
151
- """A struct that represents a request to write provenance for a quantum."""
152
-
153
- quantum_id: uuid.UUID
154
- """Unique ID for the quantum."""
155
-
156
- status: ScanStatus
157
- """Combined status for the scan and the execution of the quantum."""
158
-
159
- existing_outputs: set[uuid.UUID] = dataclasses.field(default_factory=set)
160
- """Unique IDs of the output datasets that were actually written."""
161
-
162
- quantum: bytes = b""
163
- """Serialized quantum provenance model.
164
-
165
- This may be empty for quanta that had no attempts.
166
- """
167
-
168
- metadata: bytes = b""
169
- """Serialized task metadata."""
170
-
171
- logs: bytes = b""
172
- """Serialized logs."""
173
-
174
- is_compressed: bool = False
175
- """Whether the `quantum`, `metadata`, and `log` attributes are
176
- compressed.
177
- """