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

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 IncompleteReadError(BgdError): 

115 """ByteStream service read didn't return a full payload.""" 

116 

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) 

119 

120 

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.""" 

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 PermissionDeniedError(BgdError): 

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

132 

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) 

135 

136 

137class BotSessionError(BgdError): 

138 """Parent class of BotSession Exceptions""" 

139 

140 

141class BotSessionClosedError(BotSessionError): 

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

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 UnknownBotSessionError(BotSessionError): 

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

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 BotSessionMismatchError(BotSessionError): 

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

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 DuplicateBotSessionError(BotSessionError): 

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

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 BotSessionCancelledError(BotSessionError): 

170 """The BotSession update was cancelled""" 

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 DatabaseError(BgdError): 

177 """BuildGrid encountered a database error""" 

178 

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) 

181 

182 

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 """ 

188 

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) 

196 

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) 

205 

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 

213 

214 

215class RetriableDatabaseError(RetriableError): 

216 """BuildGrid encountered a retriable database error""" 

217 

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) 

222 

223 

224class ResourceExhaustedError(BgdError): 

225 """Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.""" 

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) 

229 

230 

231class StorageFullError(ResourceExhaustedError): 

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

233 

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

235 super().__init__(message, detail=detail, reason=reason) 

236 

237 

238class GrpcUninitializedError(BgdError): 

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

240 

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)