Coverage for /builds/BuildGrid/buildgrid/buildgrid/_exceptions.py: 89.66%

87 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-06-11 15:37 +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. 

14 

15 

16""" 

17Exceptions 

18=========== 

19""" 

20 

21 

22from collections import namedtuple 

23from datetime import timedelta 

24from enum import Enum 

25from typing import Any, Optional 

26 

27import grpc 

28from google.protobuf.duration_pb2 import Duration 

29 

30from buildgrid._protos.google.rpc import error_details_pb2, status_pb2 

31 

32 

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 

38 

39 

40class ErrorDomain(Enum): 

41 SERVER = 1 

42 BOT = 2 

43 

44 

45class BgdError(Exception): 

46 """ 

47 Base BuildGrid Error class for internal exceptions. 

48 """ 

49 

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) 

59 

60 # Additional detail and extra information 

61 self.detail = detail 

62 

63 # Domand and reason 

64 self.domain = domain 

65 self.reason = reason 

66 

67 

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) 

71 

72 

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) 

76 

77 

78class CancelledError(BgdError): 

79 """The job was cancelled and any callers should be notified""" 

80 

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 

84 

85 

86class InvalidArgumentError(BgdError): 

87 """A bad argument was passed, such as a name which doesn't exist.""" 

88 

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) 

91 

92 

93class NotFoundError(BgdError): 

94 """Requested resource not found.""" 

95 

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) 

98 

99 

100class UpdateNotAllowedError(BgdError): 

101 """UpdateNotAllowedError error.""" 

102 

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) 

105 

106 

107class OutOfRangeError(BgdError): 

108 """ByteStream service read data out of range.""" 

109 

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) 

112 

113 

114class FailedPreconditionError(BgdError): 

115 """One or more errors occurred in setting up the action requested, such as 

116 a missing input or command or no worker being available. The client may be 

117 able to fix the errors and retry.""" 

118 

119 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None: 

120 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason) 

121 

122 

123class PermissionDeniedError(BgdError): 

124 """The caller does not have permission to execute the specified operation.""" 

125 

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) 

128 

129 

130class BotSessionError(BgdError): 

131 """Parent class of BotSession Exceptions""" 

132 

133 

134class BotSessionClosedError(BotSessionError): 

135 """The requested BotSession has been closed recently.""" 

136 

137 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None: 

138 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason) 

139 

140 

141class UnknownBotSessionError(BotSessionError): 

142 """Buildgrid does not know the requested BotSession.""" 

143 

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) 

146 

147 

148class BotSessionMismatchError(BotSessionError): 

149 """The BotSession details don't match those in BuildGrid's records.""" 

150 

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) 

153 

154 

155class DuplicateBotSessionError(BotSessionError): 

156 """The bot with this ID already has a BotSession.""" 

157 

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) 

160 

161 

162class BotSessionCancelledError(BotSessionError): 

163 """The BotSession update was cancelled""" 

164 

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) 

167 

168 

169class DatabaseError(BgdError): 

170 """BuildGrid encountered a database error""" 

171 

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) 

174 

175 

176class RetriableError(BgdError): 

177 """BuildGrid encountered a retriable error 

178 `retry_info` to instruct clients when to retry 

179 `error_status` a grpc.Status message suitable to call with context.abort_with_status() 

180 """ 

181 

182 def __init__( 

183 self, message: str, retry_period: timedelta, detail: Optional[Any] = None, reason: Optional[Any] = None 

184 ) -> None: 

185 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason) 

186 retry_delay = Duration() 

187 retry_delay.FromTimedelta(retry_period) 

188 retry_info = error_details_pb2.RetryInfo(retry_delay=retry_delay) 

189 

190 # We could get the integer value of the UNAVAILABLE 

191 # status code using grpc.StatusCode.UNAVAILABLE.value[0], 

192 # but the grpc-stubs library complains if we do that. So 

193 # instead we just hardcode the value, which is unlikely to 

194 # change as it's a standard code: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md 

195 status_proto = status_pb2.Status(code=14, message=message) 

196 error_detail = status_proto.details.add() 

197 error_detail.Pack(retry_info) 

198 

199 error_status = _Status( 

200 code=grpc.StatusCode.UNAVAILABLE, 

201 details=status_proto.message, 

202 trailing_metadata=(("grpc-status-details-bin", status_proto.SerializeToString()),), 

203 ) 

204 self.retry_info = retry_info 

205 self.error_status = error_status 

206 

207 

208class RetriableDatabaseError(RetriableError): 

209 """BuildGrid encountered a retriable database error""" 

210 

211 def __init__( 

212 self, message: str, retry_period: timedelta, detail: Optional[Any] = None, reason: Optional[Any] = None 

213 ) -> None: 

214 super().__init__(message, retry_period, detail=detail, reason=reason) 

215 

216 

217class StorageFullError(BgdError): 

218 """BuildGrid's storage is full, cannot commit to it.""" 

219 

220 def __init__(self, message: Optional[str], detail: Optional[Any] = None, reason: Optional[Any] = None) -> None: 

221 super().__init__(message, detail=detail, domain=ErrorDomain.SERVER, reason=reason) 

222 

223 

224class GrpcUninitializedError(BgdError): 

225 """BuildGrid tried to use a gRPC stub before gRPC was initialized.""" 

226 

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)