Coverage for python/lsst/ctrl/mpexec/reports.py: 82%
66 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 10:28 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 10:28 -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 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/>.
28from __future__ import annotations
30__all__ = ["ExceptionInfo", "ExecutionStatus", "Report", "QuantumReport"]
32import enum
33import sys
34from typing import Any
36import pydantic
37from lsst.daf.butler import DataCoordinate, DataId, DataIdValue
38from lsst.pipe.base import QgraphSummary
39from lsst.utils.introspection import get_full_type_name
42def _serializeDataId(dataId: DataId) -> dict[str, DataIdValue]:
43 if isinstance(dataId, DataCoordinate):
44 return dict(dataId.required)
45 else:
46 return dataId # type: ignore
49class ExecutionStatus(enum.Enum):
50 """Possible values for job execution status.
52 Status `FAILURE` is set if one or more tasks failed. Status `TIMEOUT` is
53 set if there are no failures but one or more tasks timed out. Timeouts can
54 only be detected in multi-process mode, child task is killed on timeout
55 and usually should have non-zero exit code.
56 """
58 SUCCESS = "success"
59 FAILURE = "failure"
60 TIMEOUT = "timeout"
61 SKIPPED = "skipped"
64class ExceptionInfo(pydantic.BaseModel):
65 """Information about exception."""
67 className: str
68 """Name of the exception class if exception was raised."""
70 message: str
71 """Exception message for in-process quantum execution, None if
72 quantum was executed in sub-process.
73 """
75 @classmethod
76 def from_exception(cls, exception: Exception) -> ExceptionInfo:
77 """Construct instance from an exception.
79 Parameters
80 ----------
81 exception : `Exception`
82 Exception to wrap.
84 Returns
85 -------
86 info : `ExceptionInfo`
87 Information about the exception.
88 """
89 return cls(className=get_full_type_name(exception), message=str(exception))
92class QuantumReport(pydantic.BaseModel):
93 """Task execution report for a single Quantum.
95 Parameters
96 ----------
97 dataId : `~lsst.daf.butler.DataId`
98 Quantum data ID.
99 taskLabel : `str`
100 Label for task executing this Quantum.
101 status : `ExecutionStatus`
102 Status of this quantum execution.
103 exitCode : `int` or `None`, optional
104 Exit code for sub-process executing this Quantum. `None` for
105 in-process execution. Negative if process was killed by a signal.
106 exceptionInfo : `ExceptionInfo` or `None`, optional
107 Exception information if an exception was raised.
108 """
110 status: ExecutionStatus = ExecutionStatus.SUCCESS
111 """Execution status, one of the values in `ExecutionStatus` enum."""
113 dataId: dict[str, DataIdValue]
114 """Quantum DataId."""
116 taskLabel: str | None
117 """Label for a task executing this Quantum."""
119 exitCode: int | None = None
120 """Exit code for a sub-process executing Quantum, None for in-process
121 Quantum execution. Negative if process was killed by a signal.
122 """
124 exceptionInfo: ExceptionInfo | None = None
125 """Exception information if exception was raised."""
127 def __init__(
128 self,
129 dataId: DataId,
130 taskLabel: str,
131 status: ExecutionStatus = ExecutionStatus.SUCCESS,
132 exitCode: int | None = None,
133 exceptionInfo: ExceptionInfo | None = None,
134 ):
135 super().__init__(
136 status=status,
137 dataId=_serializeDataId(dataId),
138 taskLabel=taskLabel,
139 exitCode=exitCode,
140 exceptionInfo=exceptionInfo,
141 )
143 @classmethod
144 def from_exception(
145 cls,
146 exception: Exception,
147 dataId: DataId,
148 taskLabel: str,
149 ) -> QuantumReport:
150 """Construct report instance from an exception and other pieces of
151 data.
153 Parameters
154 ----------
155 exception : `Exception`
156 Exception caught from processing quantum.
157 dataId : `~lsst.daf.butler.DataId`
158 Data ID of quantum.
159 taskLabel : `str`
160 Label of task.
161 """
162 return cls(
163 status=ExecutionStatus.FAILURE,
164 dataId=dataId,
165 taskLabel=taskLabel,
166 exceptionInfo=ExceptionInfo.from_exception(exception),
167 )
169 @classmethod
170 def from_exit_code(
171 cls,
172 exitCode: int,
173 dataId: DataId,
174 taskLabel: str,
175 ) -> QuantumReport:
176 """Construct report instance from an exit code and other pieces of
177 data.
179 Parameters
180 ----------
181 exitCode : `int`
182 The exit code of the subprocess.
183 dataId : `~lsst.daf.butler.DataId`
184 The quantum Data ID.
185 taskLabel : `str`
186 The task label.
187 """
188 return cls(
189 status=ExecutionStatus.SUCCESS if exitCode == 0 else ExecutionStatus.FAILURE,
190 dataId=dataId,
191 taskLabel=taskLabel,
192 exitCode=exitCode,
193 )
196class Report(pydantic.BaseModel):
197 """Execution report for the whole job with one or few quanta."""
199 qgraphSummary: QgraphSummary
200 """Summary report about QuantumGraph."""
202 status: ExecutionStatus = ExecutionStatus.SUCCESS
203 """Job status."""
205 cmdLine: list[str] | None = None
206 """Command line for the whole job."""
208 exitCode: int | None = None
209 """Job exit code, this obviously cannot be set in pipetask."""
211 exceptionInfo: ExceptionInfo | None = None
212 """Exception information if exception was raised."""
214 quantaReports: list[QuantumReport] = []
215 """List of per-quantum reports, ordering is not specified. Some or all
216 quanta may not produce a report.
217 """
219 # Always want to validate the default value for cmdLine so
220 # use a model_validator.
221 @pydantic.model_validator(mode="before")
222 @classmethod
223 def _set_cmdLine(cls, data: Any) -> Any:
224 if data.get("cmdLine") is None:
225 data["cmdLine"] = sys.argv
226 return data
228 def set_exception(self, exception: Exception) -> None:
229 """Update exception information from an exception object.
231 Parameters
232 ----------
233 exception : `Exception`
234 Exception to use to extract information from.
235 """
236 self.exceptionInfo = ExceptionInfo.from_exception(exception)