From cfa8010b0f4906dc216004dcbeff806bf2c47aa7 Mon Sep 17 00:00:00 2001 From: Jim Bosch Date: Fri, 23 Aug 2024 12:06:27 -0400 Subject: [PATCH 1/2] Lower logging by AnnotatedPartialOutputsError to DEBUG. It should be the responsibility of higher-level catching code to decide whether to log loudly or re-raise instead. --- python/lsst/pipe/base/_status.py | 2 +- tests/test_task.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lsst/pipe/base/_status.py b/python/lsst/pipe/base/_status.py index e74187e3..0d2f754f 100644 --- a/python/lsst/pipe/base/_status.py +++ b/python/lsst/pipe/base/_status.py @@ -202,7 +202,7 @@ def runQuantum(self, butlerQC, inputRefs, outputRefs): continue item.metadata.set_dict("failure", failure_info) # type: ignore - log.exception( + log.debug( "Task failed with only partial outputs; see exception message for details.", exc_info=error, ) diff --git a/tests/test_task.py b/tests/test_task.py index 3314864c..167f59b2 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -326,7 +326,7 @@ def test_annotate_exception(self): task = AddMultTask() msg = "something failed!" error = ValueError(msg) - with self.assertLogs("addMult", level="ERROR") as cm: + with self.assertLogs("addMult", level="DEBUG") as cm: pipeBase.AnnotatedPartialOutputsError.annotate(error, task, log=task.log) self.assertIn(msg, "\n".join(cm.output)) self.assertEqual(task.metadata["failure"]["message"], msg) @@ -346,7 +346,7 @@ def metadata(self): task = AddMultTask() msg = "something failed!" error = TestError(msg) - with self.assertLogs("addMult", level="ERROR") as cm: + with self.assertLogs("addMult", level="DEBUG") as cm: pipeBase.AnnotatedPartialOutputsError.annotate(error, task, log=task.log) self.assertIn(msg, "\n".join(cm.output)) self.assertEqual(task.metadata["failure"]["message"], msg) From 80c412e7bb30ee55b482f7e5a5a0c82750ea6bae Mon Sep 17 00:00:00 2001 From: Jim Bosch Date: Fri, 23 Aug 2024 12:36:54 -0400 Subject: [PATCH 2/2] Special-case AnnotatedPartialOutputsError in mocking system. We need this to raise with chaining in the mocks, since that's how it should always be used in production. --- .../pipe/base/tests/mocks/_pipeline_task.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/python/lsst/pipe/base/tests/mocks/_pipeline_task.py b/python/lsst/pipe/base/tests/mocks/_pipeline_task.py index c8bdda9b..0c2d7ca2 100644 --- a/python/lsst/pipe/base/tests/mocks/_pipeline_task.py +++ b/python/lsst/pipe/base/tests/mocks/_pipeline_task.py @@ -44,13 +44,14 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar from astropy.units import Quantity -from lsst.daf.butler import DataCoordinate, DatasetRef, DeferredDatasetHandle, SerializedDatasetType +from lsst.daf.butler import DataCoordinate, DatasetRef, DeferredDatasetHandle, Quantum, SerializedDatasetType from lsst.pex.config import Config, ConfigDictField, ConfigurableField, Field, ListField from lsst.utils.doImport import doImportType from lsst.utils.introspection import get_full_type_name from lsst.utils.iteration import ensure_iterable from ... import connectionTypes as cT +from ..._status import AnnotatedPartialOutputsError, RepeatableQuantumError from ...config import PipelineTaskConfig from ...connections import InputQuantizedConnection, OutputQuantizedConnection, PipelineTaskConnections from ...pipeline_graph import PipelineGraph @@ -291,7 +292,7 @@ def runQuantum( # Possibly raise an exception. if self.data_id_match is not None and self.data_id_match.match(quantum.dataId): assert self.fail_exception is not None, "Exception type must be defined" - message = f"Simulated failure: task={self.getName()} dataId={quantum.dataId}" + if self.memory_required is not None: if butlerQC.resources.max_mem < self.memory_required: _LOG.info( @@ -299,10 +300,10 @@ def runQuantum( self.getName(), quantum.dataId, ) - raise self.fail_exception(message) + self._fail(quantum) else: _LOG.info("Simulating failure of task '%s' on quantum %s", self.getName(), quantum.dataId) - raise self.fail_exception(message) + self._fail(quantum) # Populate the bit of provenance we store in all outputs. _LOG.info("Reading input data for task '%s' on quantum %s", self.getName(), quantum.dataId) @@ -351,6 +352,25 @@ def runQuantum( _LOG.info("Finished mocking task '%s' on quantum %s", self.getName(), quantum.dataId) + def _fail(self, quantum: Quantum) -> None: + """Raise the configured exception. + + Parameters + ---------- + quantum : `lsst.daf.butler.Quantum` + Quantum producing the error. + """ + message = f"Simulated failure: task={self.getName()} dataId={quantum.dataId}" + if self.fail_exception is AnnotatedPartialOutputsError: + # This exception is expected to always chain another. + try: + raise RepeatableQuantumError(message) + except RepeatableQuantumError as err: + raise AnnotatedPartialOutputsError() from err + else: + assert self.fail_exception is not None, "Method should not be called." + raise self.fail_exception(message) + class MockPipelineDefaultTargetConnections(PipelineTaskConnections, dimensions=()): pass