Coverage for python/lsst/ctrl/mpexec/singleQuantumExecutor.py: 12%
283 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-19 05:03 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-19 05:03 -0700
1# This file is part of ctrl_mpexec.
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 program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22__all__ = ["SingleQuantumExecutor"]
24# -------------------------------
25# Imports of standard modules --
26# -------------------------------
27import logging
28import os
29import shutil
30import sys
31import tempfile
32import time
33from collections import defaultdict
34from contextlib import contextmanager
35from itertools import chain
36from logging import FileHandler
37from typing import Any, Iterator, Optional, Union
39from lsst.daf.butler import Butler, DatasetRef, DatasetType, FileDataset, NamedKeyDict, Quantum
40from lsst.daf.butler.core.logging import ButlerLogRecordHandler, ButlerLogRecords, ButlerMDC, JsonLogFormatter
41from lsst.pipe.base import (
42 AdjustQuantumHelper,
43 ButlerQuantumContext,
44 Instrument,
45 InvalidQuantumError,
46 NoWorkFound,
47 PipelineTask,
48 PipelineTaskConfig,
49 RepeatableQuantumError,
50 TaskDef,
51 TaskFactory,
52)
53from lsst.pipe.base.configOverrides import ConfigOverrides
55# During metadata transition phase, determine metadata class by
56# asking pipe_base
57from lsst.pipe.base.task import _TASK_FULL_METADATA_TYPE, _TASK_METADATA_TYPE
58from lsst.utils.timer import logInfo
60# -----------------------------
61# Imports for other modules --
62# -----------------------------
63from .cli.utils import _PipelineAction
64from .mock_task import MockButlerQuantumContext, MockPipelineTask
65from .quantumGraphExecutor import QuantumExecutor
66from .reports import QuantumReport
68# ----------------------------------
69# Local non-exported definitions --
70# ----------------------------------
72_LOG = logging.getLogger(__name__)
75class _LogCaptureFlag:
76 """Simple flag to enable/disable log-to-butler saving."""
78 store: bool = True
81class SingleQuantumExecutor(QuantumExecutor):
82 """Executor class which runs one Quantum at a time.
84 Parameters
85 ----------
86 butler : `~lsst.daf.butler.Butler`
87 Data butler.
88 taskFactory : `~lsst.pipe.base.TaskFactory`
89 Instance of a task factory.
90 skipExistingIn : `list` [ `str` ], optional
91 Accepts list of collections, if all Quantum outputs already exist in
92 the specified list of collections then that Quantum will not be rerun.
93 clobberOutputs : `bool`, optional
94 If `True`, then existing outputs in output run collection will be
95 overwritten. If ``skipExistingIn`` is defined, only outputs from
96 failed quanta will be overwritten.
97 enableLsstDebug : `bool`, optional
98 Enable debugging with ``lsstDebug`` facility for a task.
99 exitOnKnownError : `bool`, optional
100 If `True`, call `sys.exit` with the appropriate exit code for special
101 known exceptions, after printing a traceback, instead of letting the
102 exception propagate up to calling. This is always the behavior for
103 InvalidQuantumError.
104 mock : `bool`, optional
105 If `True` then mock task execution.
106 mock_configs : `list` [ `_PipelineAction` ], optional
107 Optional config overrides for mock tasks.
108 """
110 stream_json_logs = True
111 """If True each log record is written to a temporary file and ingested
112 when quantum completes. If False the records are accumulated in memory
113 and stored in butler on quantum completion."""
115 def __init__(
116 self,
117 taskFactory: TaskFactory,
118 skipExistingIn: Optional[list[str]] = None,
119 clobberOutputs: bool = False,
120 enableLsstDebug: bool = False,
121 exitOnKnownError: bool = False,
122 mock: bool = False,
123 mock_configs: Optional[list[_PipelineAction]] = None,
124 ):
125 self.taskFactory = taskFactory
126 self.skipExistingIn = skipExistingIn
127 self.enableLsstDebug = enableLsstDebug
128 self.clobberOutputs = clobberOutputs
129 self.exitOnKnownError = exitOnKnownError
130 self.mock = mock
131 self.mock_configs = mock_configs if mock_configs is not None else []
132 self.log_handler: Optional[logging.Handler] = None
133 self.report: Optional[QuantumReport] = None
135 def execute(self, taskDef: TaskDef, quantum: Quantum, butler: Butler) -> Quantum:
136 # Docstring inherited from QuantumExecutor.execute
138 # Catch any exception and make a report based on that.
139 try:
140 result = self._execute(taskDef, quantum, butler)
141 self.report = QuantumReport(dataId=quantum.dataId, taskLabel=taskDef.label)
142 return result
143 except Exception as exc:
144 assert quantum.dataId is not None, "Quantum DataId cannot be None"
145 self.report = QuantumReport.from_exception(
146 exception=exc,
147 dataId=quantum.dataId,
148 taskLabel=taskDef.label,
149 )
150 raise
152 def _execute(self, taskDef: TaskDef, quantum: Quantum, butler: Butler) -> Quantum:
153 """Internal implementation of execute()"""
154 startTime = time.time()
156 with self.captureLogging(taskDef, quantum, butler) as captureLog:
158 # Save detailed resource usage before task start to metadata.
159 quantumMetadata = _TASK_METADATA_TYPE()
160 logInfo(None, "prep", metadata=quantumMetadata) # type: ignore
162 taskClass, label, config = taskDef.taskClass, taskDef.label, taskDef.config
164 # check whether to skip or delete old outputs, if it returns True
165 # or raises an exception do not try to store logs, as they may be
166 # already in butler.
167 captureLog.store = False
168 if self.checkExistingOutputs(quantum, butler, taskDef):
169 _LOG.info(
170 "Skipping already-successful quantum for label=%s dataId=%s.", label, quantum.dataId
171 )
172 return quantum
173 captureLog.store = True
175 try:
176 quantum = self.updatedQuantumInputs(quantum, butler, taskDef)
177 except NoWorkFound as exc:
178 _LOG.info(
179 "Nothing to do for task '%s' on quantum %s; saving metadata and skipping: %s",
180 taskDef.label,
181 quantum.dataId,
182 str(exc),
183 )
184 # Make empty metadata that looks something like what a
185 # do-nothing task would write (but we don't bother with empty
186 # nested PropertySets for subtasks). This is slightly
187 # duplicative with logic in pipe_base that we can't easily call
188 # from here; we'll fix this on DM-29761.
189 logInfo(None, "end", metadata=quantumMetadata) # type: ignore
190 fullMetadata = _TASK_FULL_METADATA_TYPE()
191 fullMetadata[taskDef.label] = _TASK_METADATA_TYPE()
192 fullMetadata["quantum"] = quantumMetadata
193 self.writeMetadata(quantum, fullMetadata, taskDef, butler)
194 return quantum
196 # enable lsstDebug debugging
197 if self.enableLsstDebug:
198 try:
199 _LOG.debug("Will try to import debug.py")
200 import debug # type: ignore # noqa:F401
201 except ImportError:
202 _LOG.warn("No 'debug' module found.")
204 # initialize global state
205 self.initGlobals(quantum, butler)
207 # Ensure that we are executing a frozen config
208 config.freeze()
209 logInfo(None, "init", metadata=quantumMetadata) # type: ignore
210 task = self.makeTask(taskClass, label, config, butler)
211 logInfo(None, "start", metadata=quantumMetadata) # type: ignore
212 try:
213 if self.mock:
214 # Use mock task instance to execute method.
215 runTask = self._makeMockTask(taskDef)
216 else:
217 runTask = task
218 self.runQuantum(runTask, quantum, taskDef, butler)
219 except Exception as e:
220 _LOG.error(
221 "Execution of task '%s' on quantum %s failed. Exception %s: %s",
222 taskDef.label,
223 quantum.dataId,
224 e.__class__.__name__,
225 str(e),
226 )
227 raise
228 logInfo(None, "end", metadata=quantumMetadata) # type: ignore
229 fullMetadata = task.getFullMetadata()
230 fullMetadata["quantum"] = quantumMetadata
231 self.writeMetadata(quantum, fullMetadata, taskDef, butler)
232 stopTime = time.time()
233 _LOG.info(
234 "Execution of task '%s' on quantum %s took %.3f seconds",
235 taskDef.label,
236 quantum.dataId,
237 stopTime - startTime,
238 )
239 return quantum
241 def _makeMockTask(self, taskDef: TaskDef) -> PipelineTask:
242 """Make an instance of mock task for given TaskDef."""
243 # Make config instance and apply overrides
244 overrides = ConfigOverrides()
245 for action in self.mock_configs:
246 if action.label == taskDef.label + "-mock":
247 if action.action == "config":
248 key, _, value = action.value.partition("=")
249 overrides.addValueOverride(key, value)
250 elif action.action == "configfile":
251 overrides.addFileOverride(os.path.expandvars(action.value))
252 else:
253 raise ValueError(f"Unexpected action for mock task config overrides: {action}")
254 config = MockPipelineTask.ConfigClass()
255 overrides.applyTo(config)
257 task = MockPipelineTask(config=config, name=taskDef.label)
258 return task
260 @contextmanager
261 def captureLogging(self, taskDef: TaskDef, quantum: Quantum, butler: Butler) -> Iterator:
262 """Configure logging system to capture logs for execution of this task.
264 Parameters
265 ----------
266 taskDef : `lsst.pipe.base.TaskDef`
267 The task definition.
268 quantum : `~lsst.daf.butler.Quantum`
269 Single Quantum instance.
270 butler : `~lsst.daf.butler.Butler`
271 Butler to write logs to.
273 Notes
274 -----
275 Expected to be used as a context manager to ensure that logging
276 records are inserted into the butler once the quantum has been
277 executed:
279 .. code-block:: py
281 with self.captureLogging(taskDef, quantum, butler):
282 # Run quantum and capture logs.
284 Ths method can also setup logging to attach task- or
285 quantum-specific information to log messages. Potentially this can
286 take into account some info from task configuration as well.
287 """
288 # Add a handler to the root logger to capture execution log output.
289 # How does it get removed reliably?
290 if taskDef.logOutputDatasetName is not None:
291 # Either accumulate into ButlerLogRecords or stream
292 # JSON records to file and ingest that.
293 tmpdir = None
294 if self.stream_json_logs:
295 # Create the log file in a temporary directory rather than
296 # creating a temporary file. This is necessary because
297 # temporary files are created with restrictive permissions
298 # and during file ingest these permissions persist in the
299 # datastore. Using a temp directory allows us to create
300 # a file with umask default permissions.
301 tmpdir = tempfile.mkdtemp(prefix="butler-temp-logs-")
303 # Construct a file to receive the log records and "touch" it.
304 log_file = os.path.join(tmpdir, f"butler-log-{taskDef.label}.json")
305 with open(log_file, "w"):
306 pass
307 self.log_handler = FileHandler(log_file)
308 self.log_handler.setFormatter(JsonLogFormatter())
309 else:
310 self.log_handler = ButlerLogRecordHandler()
312 logging.getLogger().addHandler(self.log_handler)
314 # include quantum dataId and task label into MDC
315 label = taskDef.label
316 if quantum.dataId:
317 label += f":{quantum.dataId}"
319 ctx = _LogCaptureFlag()
320 try:
321 with ButlerMDC.set_mdc({"LABEL": label, "RUN": butler.run or ""}):
322 yield ctx
323 finally:
324 # Ensure that the logs are stored in butler.
325 self.writeLogRecords(quantum, taskDef, butler, ctx.store)
326 if tmpdir:
327 shutil.rmtree(tmpdir, ignore_errors=True)
329 def checkExistingOutputs(self, quantum: Quantum, butler: Butler, taskDef: TaskDef) -> bool:
330 """Decide whether this quantum needs to be executed.
332 If only partial outputs exist then they are removed if
333 ``clobberOutputs`` is True, otherwise an exception is raised.
335 Parameters
336 ----------
337 quantum : `~lsst.daf.butler.Quantum`
338 Quantum to check for existing outputs
339 butler : `~lsst.daf.butler.Butler`
340 Data butler.
341 taskDef : `~lsst.pipe.base.TaskDef`
342 Task definition structure.
344 Returns
345 -------
346 exist : `bool`
347 `True` if ``self.skipExistingIn`` is defined, and a previous
348 execution of this quanta appears to have completed successfully
349 (either because metadata was written or all datasets were written).
350 `False` otherwise.
352 Raises
353 ------
354 RuntimeError
355 Raised if some outputs exist and some not.
356 """
357 if self.skipExistingIn and taskDef.metadataDatasetName is not None:
358 # Metadata output exists; this is sufficient to assume the previous
359 # run was successful and should be skipped.
360 ref = butler.registry.findDataset(
361 taskDef.metadataDatasetName, quantum.dataId, collections=self.skipExistingIn
362 )
363 if ref is not None:
364 if butler.datastore.exists(ref):
365 return True
367 # Previously we always checked for existing outputs in `butler.run`,
368 # now logic gets more complicated as we only want to skip quantum
369 # whose outputs exist in `self.skipExistingIn` but pruning should only
370 # be done for outputs existing in `butler.run`.
372 def findOutputs(
373 collections: Optional[Union[str, list[str]]]
374 ) -> tuple[list[DatasetRef], list[DatasetRef]]:
375 """Find quantum outputs in specified collections."""
376 existingRefs = []
377 missingRefs = []
378 for datasetRefs in quantum.outputs.values():
379 checkRefs: list[DatasetRef] = []
380 registryRefToQuantumRef: dict[DatasetRef, DatasetRef] = {}
381 for datasetRef in datasetRefs:
382 ref = butler.registry.findDataset(
383 datasetRef.datasetType, datasetRef.dataId, collections=collections
384 )
385 if ref is None:
386 missingRefs.append(datasetRef)
387 else:
388 checkRefs.append(ref)
389 registryRefToQuantumRef[ref] = datasetRef
391 # More efficient to ask the datastore in bulk for ref
392 # existence rather than one at a time.
393 existence = butler.datastore.mexists(checkRefs)
394 for ref, exists in existence.items():
395 if exists:
396 existingRefs.append(ref)
397 else:
398 missingRefs.append(registryRefToQuantumRef[ref])
399 return existingRefs, missingRefs
401 existingRefs, missingRefs = findOutputs(self.skipExistingIn)
402 if self.skipExistingIn:
403 if existingRefs and not missingRefs:
404 # everything is already there
405 return True
407 # If we are to re-run quantum then prune datasets that exists in
408 # output run collection, only if `self.clobberOutputs` is set.
409 if existingRefs:
410 existingRefs, missingRefs = findOutputs(butler.run)
411 if existingRefs and missingRefs:
412 _LOG.debug(
413 "Partial outputs exist for task %s dataId=%s collection=%s "
414 "existingRefs=%s missingRefs=%s",
415 taskDef,
416 quantum.dataId,
417 butler.run,
418 existingRefs,
419 missingRefs,
420 )
421 if self.clobberOutputs:
422 # only prune
423 _LOG.info("Removing partial outputs for task %s: %s", taskDef, existingRefs)
424 butler.pruneDatasets(existingRefs, disassociate=True, unstore=True, purge=True)
425 return False
426 else:
427 raise RuntimeError(
428 f"Registry inconsistency while checking for existing outputs:"
429 f" collection={butler.run} existingRefs={existingRefs}"
430 f" missingRefs={missingRefs}"
431 )
433 # need to re-run
434 return False
436 def makeTask(
437 self, taskClass: type[PipelineTask], name: str, config: PipelineTaskConfig, butler: Butler
438 ) -> PipelineTask:
439 """Make new task instance.
441 Parameters
442 ----------
443 taskClass : `type`
444 Sub-class of `~lsst.pipe.base.PipelineTask`.
445 name : `str`
446 Name for this task.
447 config : `~lsst.pipe.base.PipelineTaskConfig`
448 Configuration object for this task
450 Returns
451 -------
452 task : `~lsst.pipe.base.PipelineTask`
453 Instance of ``taskClass`` type.
454 butler : `~lsst.daf.butler.Butler`
455 Data butler.
456 """
457 # call task factory for that
458 return self.taskFactory.makeTask(taskClass, name, config, None, butler)
460 def updatedQuantumInputs(self, quantum: Quantum, butler: Butler, taskDef: TaskDef) -> Quantum:
461 """Update quantum with extra information, returns a new updated
462 Quantum.
464 Some methods may require input DatasetRefs to have non-None
465 ``dataset_id``, but in case of intermediate dataset it may not be
466 filled during QuantumGraph construction. This method will retrieve
467 missing info from registry.
469 Parameters
470 ----------
471 quantum : `~lsst.daf.butler.Quantum`
472 Single Quantum instance.
473 butler : `~lsst.daf.butler.Butler`
474 Data butler.
475 taskDef : `~lsst.pipe.base.TaskDef`
476 Task definition structure.
478 Returns
479 -------
480 update : `~lsst.daf.butler.Quantum`
481 Updated Quantum instance
482 """
483 anyChanges = False
484 updatedInputs: defaultdict[DatasetType, list] = defaultdict(list)
485 for key, refsForDatasetType in quantum.inputs.items():
486 newRefsForDatasetType = updatedInputs[key]
487 for ref in refsForDatasetType:
488 if ref.id is None:
489 resolvedRef = butler.registry.findDataset(
490 ref.datasetType, ref.dataId, collections=butler.collections
491 )
492 if resolvedRef is None:
493 _LOG.info("No dataset found for %s", ref)
494 continue
495 else:
496 _LOG.debug("Updated dataset ID for %s", ref)
497 else:
498 resolvedRef = ref
499 # We need to ask datastore if the dataset actually exists
500 # because the Registry of a local "execution butler" cannot
501 # know this (because we prepopulate it with all of the datasets
502 # that might be created). In case of mock execution we check
503 # that mock dataset exists instead.
504 if self.mock:
505 try:
506 typeName, component = ref.datasetType.nameAndComponent()
507 if component is not None:
508 mockDatasetTypeName = MockButlerQuantumContext.mockDatasetTypeName(typeName)
509 else:
510 mockDatasetTypeName = MockButlerQuantumContext.mockDatasetTypeName(
511 ref.datasetType.name
512 )
514 mockDatasetType = butler.registry.getDatasetType(mockDatasetTypeName)
515 except KeyError:
516 # means that mock dataset type is not there and this
517 # should be a pre-existing dataset
518 _LOG.debug("No mock dataset type for %s", ref)
519 if butler.datastore.exists(resolvedRef):
520 newRefsForDatasetType.append(resolvedRef)
521 else:
522 mockRef = DatasetRef(mockDatasetType, ref.dataId)
523 resolvedMockRef = butler.registry.findDataset(
524 mockRef.datasetType, mockRef.dataId, collections=butler.collections
525 )
526 _LOG.debug("mockRef=%s resolvedMockRef=%s", mockRef, resolvedMockRef)
527 if resolvedMockRef is not None and butler.datastore.exists(resolvedMockRef):
528 _LOG.debug("resolvedMockRef dataset exists")
529 newRefsForDatasetType.append(resolvedRef)
530 elif butler.datastore.exists(resolvedRef):
531 newRefsForDatasetType.append(resolvedRef)
533 if len(newRefsForDatasetType) != len(refsForDatasetType):
534 anyChanges = True
535 # If we removed any input datasets, let the task check if it has enough
536 # to proceed and/or prune related datasets that it also doesn't
537 # need/produce anymore. It will raise NoWorkFound if it can't run,
538 # which we'll let propagate up. This is exactly what we run during QG
539 # generation, because a task shouldn't care whether an input is missing
540 # because some previous task didn't produce it, or because it just
541 # wasn't there during QG generation.
542 namedUpdatedInputs = NamedKeyDict[DatasetType, list[DatasetRef]](updatedInputs.items())
543 helper = AdjustQuantumHelper(namedUpdatedInputs, quantum.outputs)
544 if anyChanges:
545 assert quantum.dataId is not None, "Quantum DataId cannot be None"
546 helper.adjust_in_place(taskDef.connections, label=taskDef.label, data_id=quantum.dataId)
547 return Quantum(
548 taskName=quantum.taskName,
549 taskClass=quantum.taskClass,
550 dataId=quantum.dataId,
551 initInputs=quantum.initInputs,
552 inputs=helper.inputs,
553 outputs=helper.outputs,
554 )
556 def runQuantum(self, task: PipelineTask, quantum: Quantum, taskDef: TaskDef, butler: Butler) -> None:
557 """Execute task on a single quantum.
559 Parameters
560 ----------
561 task : `~lsst.pipe.base.PipelineTask`
562 Task object.
563 quantum : `~lsst.daf.butler.Quantum`
564 Single Quantum instance.
565 taskDef : `~lsst.pipe.base.TaskDef`
566 Task definition structure.
567 butler : `~lsst.daf.butler.Butler`
568 Data butler.
569 """
570 # Create a butler that operates in the context of a quantum
571 if not self.mock:
572 butlerQC = ButlerQuantumContext(butler, quantum)
573 else:
574 butlerQC = MockButlerQuantumContext(butler, quantum)
576 # Get the input and output references for the task
577 inputRefs, outputRefs = taskDef.connections.buildDatasetRefs(quantum)
579 # Call task runQuantum() method. Catch a few known failure modes and
580 # translate them into specific
581 try:
582 task.runQuantum(butlerQC, inputRefs, outputRefs)
583 except NoWorkFound as err:
584 # Not an error, just an early exit.
585 _LOG.info("Task '%s' on quantum %s exited early: %s", taskDef.label, quantum.dataId, str(err))
586 pass
587 except RepeatableQuantumError as err:
588 if self.exitOnKnownError:
589 _LOG.warning("Caught repeatable quantum error for %s (%s):", taskDef, quantum.dataId)
590 _LOG.warning(err, exc_info=True)
591 sys.exit(err.EXIT_CODE)
592 else:
593 raise
594 except InvalidQuantumError as err:
595 _LOG.fatal("Invalid quantum error for %s (%s): %s", taskDef, quantum.dataId)
596 _LOG.fatal(err, exc_info=True)
597 sys.exit(err.EXIT_CODE)
599 def writeMetadata(self, quantum: Quantum, metadata: Any, taskDef: TaskDef, butler: Butler) -> None:
600 if taskDef.metadataDatasetName is not None:
601 # DatasetRef has to be in the Quantum outputs, can lookup by name
602 try:
603 ref = quantum.outputs[taskDef.metadataDatasetName]
604 except LookupError as exc:
605 raise InvalidQuantumError(
606 f"Quantum outputs is missing metadata dataset type {taskDef.metadataDatasetName};"
607 f" this could happen due to inconsistent options between QuantumGraph generation"
608 f" and execution"
609 ) from exc
610 butler.put(metadata, ref[0])
612 def writeLogRecords(self, quantum: Quantum, taskDef: TaskDef, butler: Butler, store: bool) -> None:
613 # If we are logging to an external file we must always try to
614 # close it.
615 filename = None
616 if isinstance(self.log_handler, FileHandler):
617 filename = self.log_handler.stream.name
618 self.log_handler.close()
620 if self.log_handler is not None:
621 # Remove the handler so we stop accumulating log messages.
622 logging.getLogger().removeHandler(self.log_handler)
624 try:
625 if store and taskDef.logOutputDatasetName is not None and self.log_handler is not None:
626 # DatasetRef has to be in the Quantum outputs, can lookup by
627 # name
628 try:
629 ref = quantum.outputs[taskDef.logOutputDatasetName]
630 except LookupError as exc:
631 raise InvalidQuantumError(
632 f"Quantum outputs is missing log output dataset type {taskDef.logOutputDatasetName};"
633 f" this could happen due to inconsistent options between QuantumGraph generation"
634 f" and execution"
635 ) from exc
637 if isinstance(self.log_handler, ButlerLogRecordHandler):
638 butler.put(self.log_handler.records, ref[0])
640 # Clear the records in case the handler is reused.
641 self.log_handler.records.clear()
642 else:
643 assert filename is not None, "Somehow unable to extract filename from file handler"
645 # Need to ingest this file directly into butler.
646 dataset = FileDataset(path=filename, refs=ref[0])
647 try:
648 butler.ingest(dataset, transfer="move")
649 filename = None
650 except NotImplementedError:
651 # Some datastores can't receive files (e.g. in-memory
652 # datastore when testing), we store empty list for
653 # those just to have a dataset. Alternative is to read
654 # the file as a ButlerLogRecords object and put it.
655 _LOG.info(
656 "Log records could not be stored in this butler because the"
657 " datastore can not ingest files, empty record list is stored instead."
658 )
659 records = ButlerLogRecords.from_records([])
660 butler.put(records, ref[0])
661 finally:
662 # remove file if it is not ingested
663 if filename is not None:
664 try:
665 os.remove(filename)
666 except OSError:
667 pass
669 def initGlobals(self, quantum: Quantum, butler: Butler) -> None:
670 """Initialize global state needed for task execution.
672 Parameters
673 ----------
674 quantum : `~lsst.daf.butler.Quantum`
675 Single Quantum instance.
676 butler : `~lsst.daf.butler.Butler`
677 Data butler.
679 Notes
680 -----
681 There is an issue with initializing filters singleton which is done
682 by instrument, to avoid requiring tasks to do it in runQuantum()
683 we do it here when any dataId has an instrument dimension. Also for
684 now we only allow single instrument, verify that all instrument
685 names in all dataIds are identical.
687 This will need revision when filter singleton disappears.
688 """
689 oneInstrument = None
690 for datasetRefs in chain(quantum.inputs.values(), quantum.outputs.values()):
691 for datasetRef in datasetRefs:
692 dataId = datasetRef.dataId
693 instrument = dataId.get("instrument")
694 if instrument is not None:
695 if oneInstrument is not None:
696 assert ( # type: ignore
697 instrument == oneInstrument
698 ), "Currently require that only one instrument is used per graph"
699 else:
700 oneInstrument = instrument
701 Instrument.fromName(instrument, butler.registry)
703 def getReport(self) -> Optional[QuantumReport]:
704 # Docstring inherited from base class
705 if self.report is None:
706 raise RuntimeError("getReport() called before execute()")
707 return self.report