Coverage for python/lsst/ctrl/mpexec/reports.py: 80%
64 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-14 19:56 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-14 19:56 +0000
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/>.
22from __future__ import annotations
24__all__ = ["ExceptionInfo", "ExecutionStatus", "Report", "QuantumReport"]
26import enum
27import sys
29from lsst.daf.butler import DataCoordinate, DataId, DataIdValue
30from lsst.utils.introspection import get_full_type_name
32try:
33 from pydantic.v1 import BaseModel, validator
34except ModuleNotFoundError:
35 from pydantic import BaseModel, validator # type: ignore
38def _serializeDataId(dataId: DataId) -> dict[str, DataIdValue]:
39 if isinstance(dataId, DataCoordinate):
40 return dataId.byName()
41 else:
42 return dataId # type: ignore
45class ExecutionStatus(enum.Enum):
46 """Possible values for job execution status.
48 Status `FAILURE` is set if one or more tasks failed. Status `TIMEOUT` is
49 set if there are no failures but one or more tasks timed out. Timeouts can
50 only be detected in multi-process mode, child task is killed on timeout
51 and usually should have non-zero exit code.
52 """
54 SUCCESS = "success"
55 FAILURE = "failure"
56 TIMEOUT = "timeout"
57 SKIPPED = "skipped"
60class ExceptionInfo(BaseModel):
61 """Information about exception."""
63 className: str
64 """Name of the exception class if exception was raised."""
66 message: str
67 """Exception message for in-process quantum execution, None if
68 quantum was executed in sub-process.
69 """
71 @classmethod
72 def from_exception(cls, exception: Exception) -> ExceptionInfo:
73 """Construct instance from an exception."""
74 return cls(className=get_full_type_name(exception), message=str(exception))
77class QuantumReport(BaseModel):
78 """Task execution report for a single Quantum."""
80 status: ExecutionStatus = ExecutionStatus.SUCCESS
81 """Execution status, one of the values in `ExecutionStatus` enum."""
83 dataId: dict[str, DataIdValue]
84 """Quantum DataId."""
86 taskLabel: str | None
87 """Label for a task executing this Quantum."""
89 exitCode: int | None = None
90 """Exit code for a sub-process executing Quantum, None for in-process
91 Quantum execution. Negative if process was killed by a signal.
92 """
94 exceptionInfo: ExceptionInfo | None = None
95 """Exception information if exception was raised."""
97 def __init__(
98 self,
99 dataId: DataId,
100 taskLabel: str,
101 status: ExecutionStatus = ExecutionStatus.SUCCESS,
102 exitCode: int | None = None,
103 exceptionInfo: ExceptionInfo | None = None,
104 ):
105 super().__init__(
106 status=status,
107 dataId=_serializeDataId(dataId),
108 taskLabel=taskLabel,
109 exitCode=exitCode,
110 exceptionInfo=exceptionInfo,
111 )
113 @classmethod
114 def from_exception(
115 cls,
116 exception: Exception,
117 dataId: DataId,
118 taskLabel: str,
119 ) -> QuantumReport:
120 """Construct report instance from an exception and other pieces of
121 data.
122 """
123 return cls(
124 status=ExecutionStatus.FAILURE,
125 dataId=dataId,
126 taskLabel=taskLabel,
127 exceptionInfo=ExceptionInfo.from_exception(exception),
128 )
130 @classmethod
131 def from_exit_code(
132 cls,
133 exitCode: int,
134 dataId: DataId,
135 taskLabel: str,
136 ) -> QuantumReport:
137 """Construct report instance from an exit code and other pieces of
138 data.
139 """
140 return cls(
141 status=ExecutionStatus.SUCCESS if exitCode == 0 else ExecutionStatus.FAILURE,
142 dataId=dataId,
143 taskLabel=taskLabel,
144 exitCode=exitCode,
145 )
148class Report(BaseModel):
149 """Execution report for the whole job with one or few quanta."""
151 status: ExecutionStatus = ExecutionStatus.SUCCESS
152 """Job status."""
154 cmdLine: list[str] | None = None
155 """Command line for the whole job."""
157 exitCode: int | None = None
158 """Job exit code, this obviously cannot be set in pipetask."""
160 exceptionInfo: ExceptionInfo | None = None
161 """Exception information if exception was raised."""
163 quantaReports: list[QuantumReport] = []
164 """List of per-quantum reports, ordering is not specified. Some or all
165 quanta may not produce a report.
166 """
168 @validator("cmdLine", always=True)
169 def _set_cmdLine(cls, v: list[str] | None) -> list[str]: # noqa: N805
170 if v is None:
171 v = sys.argv
172 return v
174 def set_exception(self, exception: Exception) -> None:
175 """Update exception information from an exception object."""
176 self.exceptionInfo = ExceptionInfo.from_exception(exception)