Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/exceptions.py: 90.32%
93 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
1# Copyright (C) 2018 Bloomberg LP
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# <http://www.apache.org/licenses/LICENSE-2.0>
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
16"""
17Exceptions
18===========
19"""
22from collections import namedtuple
23from datetime import timedelta
24from enum import Enum
25from typing import Any, Optional
27import grpc
28from google.protobuf.duration_pb2 import Duration
30from buildgrid._protos.google.rpc import error_details_pb2, status_pb2
33# grpc.Status is a metaclass class, so we derive
34# a local _Status and associate the expected Attributes
35# with it
36class _Status(namedtuple("_Status", ("code", "details", "trailing_metadata")), grpc.Status):
37 pass
40class ErrorDomain(Enum):
41 SERVER = 1
42 BOT = 2
45class BgdError(Exception):
46 """
47 Base BuildGrid Error class for internal exceptions.
48 """
50 def __init__(
51 self,
52 message: Optional[str],
53 *,
54 detail: Optional[Any] = None,
55 domain: Optional[ErrorDomain] = None,
56 reason: Optional[Any] = None,
57 ) -> None:
58 super().__init__(message)
60 # Additional detail and extra information
61 self.detail = detail
63 # Domand and reason
64 self.domain = domain
65 self.reason = reason
68class ServerError(BgdError):
69 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
70 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
73class BotError(BgdError):
74 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
75 super().__init__(message, detail=detail, domain=ErrorDomain.BOT, reason=reason)
78class CancelledError(BgdError):
79 """The job was cancelled and any callers should be notified"""
81 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
82 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
83 self.last_response = None
86class InvalidArgumentError(BgdError):
87 """A bad argument was passed, such as a name which doesn't exist."""
89 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
90 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
93class NotFoundError(BgdError):
94 """Requested resource not found."""
96 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
97 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
100class UpdateNotAllowedError(BgdError):
101 """UpdateNotAllowedError error."""
103 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
104 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
107class OutOfRangeError(BgdError):
108 """ByteStream service read data out of range."""
110 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
111 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
114class IncompleteReadError(BgdError):
115 """ByteStream service read didn't return a full payload."""
117 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
118 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
121class FailedPreconditionError(BgdError):
122 """One or more errors occurred in setting up the action requested, such as
123 a missing input or command or no worker being available. The client may be
124 able to fix the errors and retry."""
126 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
127 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
130class PermissionDeniedError(BgdError):
131 """The caller does not have permission to execute the specified operation."""
133 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
134 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
137class BotSessionError(BgdError):
138 """Parent class of BotSession Exceptions"""
141class BotSessionClosedError(BotSessionError):
142 """The requested BotSession has been closed recently."""
144 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
145 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
148class UnknownBotSessionError(BotSessionError):
149 """Buildgrid does not know the requested BotSession."""
151 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
152 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
155class BotSessionMismatchError(BotSessionError):
156 """The BotSession details don't match those in BuildGrid's records."""
158 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
159 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
162class DuplicateBotSessionError(BotSessionError):
163 """The bot with this ID already has a BotSession."""
165 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
166 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
169class BotSessionCancelledError(BotSessionError):
170 """The BotSession update was cancelled"""
172 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
173 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
176class DatabaseError(BgdError):
177 """BuildGrid encountered a database error"""
179 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
180 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
183class RetriableError(BgdError):
184 """BuildGrid encountered a retriable error
185 `retry_info` to instruct clients when to retry
186 `error_status` a grpc.Status message suitable to call with context.abort_with_status()
187 """
189 def __init__(
190 self, message: str, retry_period: timedelta, detail: Optional[Any] = None, reason: Optional[Any] = None
191 ) -> None:
192 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
193 retry_delay = Duration()
194 retry_delay.FromTimedelta(retry_period)
195 retry_info = error_details_pb2.RetryInfo(retry_delay=retry_delay)
197 # We could get the integer value of the UNAVAILABLE
198 # status code using grpc.StatusCode.UNAVAILABLE.value[0],
199 # but the grpc-stubs library complains if we do that. So
200 # instead we just hardcode the value, which is unlikely to
201 # change as it's a standard code: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
202 status_proto = status_pb2.Status(code=14, message=message)
203 error_detail = status_proto.details.add()
204 error_detail.Pack(retry_info)
206 error_status = _Status(
207 code=grpc.StatusCode.UNAVAILABLE,
208 details=status_proto.message,
209 trailing_metadata=(("grpc-status-details-bin", status_proto.SerializeToString()),),
210 )
211 self.retry_info = retry_info
212 self.error_status = error_status
215class RetriableDatabaseError(RetriableError):
216 """BuildGrid encountered a retriable database error"""
218 def __init__(
219 self, message: str, retry_period: timedelta, detail: Optional[Any] = None, reason: Optional[Any] = None
220 ) -> None:
221 super().__init__(message, retry_period, detail=detail, reason=reason)
224class ResourceExhaustedError(BgdError):
225 """Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space."""
227 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
228 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)
231class StorageFullError(ResourceExhaustedError):
232 """BuildGrid's storage is full, cannot commit to it."""
234 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
235 super().__init__(message, detail=detail, reason=reason)
238class GrpcUninitializedError(BgdError):
239 """BuildGrid tried to use a gRPC stub before gRPC was initialized."""
241 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None:
242 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason)