Coverage for python/lsst/ctrl/mpexec/reports.py: 81%
63 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 02:55 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 02:55 -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.utils.introspection import get_full_type_name
41def _serializeDataId(dataId: DataId) -> dict[str, DataIdValue]:
42 if isinstance(dataId, DataCoordinate):
43 return dict(dataId.required)
44 else:
45 return dataId # type: ignore
48class ExecutionStatus(enum.Enum):
49 """Possible values for job execution status.
51 Status `FAILURE` is set if one or more tasks failed. Status `TIMEOUT` is
52 set if there are no failures but one or more tasks timed out. Timeouts can
53 only be detected in multi-process mode, child task is killed on timeout
54 and usually should have non-zero exit code.
55 """
57 SUCCESS = "success"
58 FAILURE = "failure"
59 TIMEOUT = "timeout"
60 SKIPPED = "skipped"
63class ExceptionInfo(pydantic.BaseModel):
64 """Information about exception."""
66 className: str
67 """Name of the exception class if exception was raised."""
69 message: str
70 """Exception message for in-process quantum execution, None if
71 quantum was executed in sub-process.
72 """
74 @classmethod
75 def from_exception(cls, exception: Exception) -> ExceptionInfo:
76 """Construct instance from an exception.
78 Parameters
79 ----------
80 exception : `Exception`
81 Exception to wrap.
83 Returns
84 -------
85 info : `ExceptionInfo`
86 Information about the exception.
87 """
88 return cls(className=get_full_type_name(exception), message=str(exception))
91class QuantumReport(pydantic.BaseModel):
92 """Task execution report for a single Quantum.
94 Parameters
95 ----------
96 dataId : `~lsst.daf.butler.DataId`
97 Quantum data ID.
98 taskLabel : `str`
99 Label for task executing this Quantum.
100 status : `ExecutionStatus`
101 Status of this quantum execution.
102 exitCode : `int` or `None`, optional
103 Exit code for sub-process executing this Quantum. `None` for
104 in-process execution. Negative if process was killed by a signal.
105 exceptionInfo : `ExceptionInfo` or `None`, optional
106 Exception information if an exception was raised.
107 """
109 status: ExecutionStatus = ExecutionStatus.SUCCESS
110 """Execution status, one of the values in `ExecutionStatus` enum."""
112 dataId: dict[str, DataIdValue]
113 """Quantum DataId."""
115 taskLabel: str | None
116 """Label for a task executing this Quantum."""
118 exitCode: int | None = None
119 """Exit code for a sub-process executing Quantum, None for in-process
120 Quantum execution. Negative if process was killed by a signal.
121 """
123 exceptionInfo: ExceptionInfo | None = None
124 """Exception information if exception was raised."""
126 def __init__(
127 self,
128 dataId: DataId,
129 taskLabel: str,
130 status: ExecutionStatus = ExecutionStatus.SUCCESS,
131 exitCode: int | None = None,
132 exceptionInfo: ExceptionInfo | None = None,
133 ):
134 super().__init__(
135 status=status,
136 dataId=_serializeDataId(dataId),
137 taskLabel=taskLabel,
138 exitCode=exitCode,
139 exceptionInfo=exceptionInfo,
140 )
142 @classmethod
143 def from_exception(
144 cls,
145 exception: Exception,
146 dataId: DataId,
147 taskLabel: str,
148 ) -> QuantumReport:
149 """Construct report instance from an exception and other pieces of
150 data.
152 Parameters
153 ----------
154 exception : `Exception`
155 Exception caught from processing quantum.
156 dataId : `~lsst.daf.butler.DataId`
157 Data ID of quantum.
158 taskLabel : `str`
159 Label of task.
160 """
161 return cls(
162 status=ExecutionStatus.FAILURE,
163 dataId=dataId,
164 taskLabel=taskLabel,
165 exceptionInfo=ExceptionInfo.from_exception(exception),
166 )
168 @classmethod
169 def from_exit_code(
170 cls,
171 exitCode: int,
172 dataId: DataId,
173 taskLabel: str,
174 ) -> QuantumReport:
175 """Construct report instance from an exit code and other pieces of
176 data.
178 Parameters
179 ----------
180 exitCode : `int`
181 The exit code of the subprocess.
182 dataId : `~lsst.daf.butler.DataId`
183 The quantum Data ID.
184 taskLabel : `str`
185 The task label.
186 """
187 return cls(
188 status=ExecutionStatus.SUCCESS if exitCode == 0 else ExecutionStatus.FAILURE,
189 dataId=dataId,
190 taskLabel=taskLabel,
191 exitCode=exitCode,
192 )
195class Report(pydantic.BaseModel):
196 """Execution report for the whole job with one or few quanta."""
198 status: ExecutionStatus = ExecutionStatus.SUCCESS
199 """Job status."""
201 cmdLine: list[str] | None = None
202 """Command line for the whole job."""
204 exitCode: int | None = None
205 """Job exit code, this obviously cannot be set in pipetask."""
207 exceptionInfo: ExceptionInfo | None = None
208 """Exception information if exception was raised."""
210 quantaReports: list[QuantumReport] = []
211 """List of per-quantum reports, ordering is not specified. Some or all
212 quanta may not produce a report.
213 """
215 # Always want to validate the default value for cmdLine so
216 # use a model_validator.
217 @pydantic.model_validator(mode="before")
218 @classmethod
219 def _set_cmdLine(cls, data: Any) -> Any:
220 if data.get("cmdLine") is None:
221 data["cmdLine"] = sys.argv
222 return data
224 def set_exception(self, exception: Exception) -> None:
225 """Update exception information from an exception object.
227 Parameters
228 ----------
229 exception : `Exception`
230 Exception to use to extract information from.
231 """
232 self.exceptionInfo = ExceptionInfo.from_exception(exception)