Skip to content

Commit

Permalink
fix: add actionable errors for GCE long running operations (#498)
Browse files Browse the repository at this point in the history
* fix: add actionable errors for GCE long running operations

* add unit test

* mypy

* add notes that the workaround should be removed once proposal A from b/284179390 is implemented

* fix typo

* fix coverage
  • Loading branch information
parthea authored Jun 12, 2023
1 parent 8844edb commit 7dfc3a7
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 2 deletions.
15 changes: 13 additions & 2 deletions google/api_core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,21 @@ def __init__(self, message, errors=(), details=(), response=None, error_info=Non
self._error_info = error_info

def __str__(self):
error_msg = "{} {}".format(self.code, self.message)
if self.details:
return "{} {} {}".format(self.code, self.message, self.details)
error_msg = "{} {}".format(error_msg, self.details)
# Note: This else condition can be removed once proposal A from
# b/284179390 is implemented.
else:
return "{} {}".format(self.code, self.message)
if self.errors:
errors = [
f"{error.code}: {error.message}"
for error in self.errors
if hasattr(error, "code") and hasattr(error, "message")
]
if errors:
error_msg = "{} {}".format(error_msg, "\n".join(errors))
return error_msg

@property
def reason(self):
Expand Down
6 changes: 6 additions & 0 deletions google/api_core/extended_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,16 @@ def _handle_refreshed_operation(self):
return

if self.error_code and self.error_message:
# Note: `errors` can be removed once proposal A from
# b/284179390 is implemented.
errors = []
if hasattr(self, "error") and hasattr(self.error, "errors"):
errors = self.error.errors
exception = exceptions.from_http_status(
status_code=self.error_code,
message=self.error_message,
response=self._extended_operation,
errors=errors,
)
self.set_exception(exception)
elif self.error_code or self.error_message:
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/test_extended_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,23 @@ class StatusCode(enum.Enum):
DONE = 1
PENDING = 2

class LROCustomErrors:
class LROCustomError:
def __init__(self, code: str = "", message: str = ""):
self.code = code
self.message = message

def __init__(self, errors: typing.List[LROCustomError] = []):
self.errors = errors

name: str
status: StatusCode
error_code: typing.Optional[int] = None
error_message: typing.Optional[str] = None
armor_class: typing.Optional[int] = None
# Note: `error` can be removed once proposal A from
# b/284179390 is implemented.
error: typing.Optional[LROCustomErrors] = None

# Note: in generated clients, this property must be generated for each
# extended operation message type.
Expand Down Expand Up @@ -170,6 +182,35 @@ def test_error():
with pytest.raises(exceptions.BadRequest):
ex_op.result()

# Test GCE custom LRO Error. See b/284179390
# Note: This test case can be removed once proposal A from
# b/284179390 is implemented.
_EXCEPTION_CODE = "INCOMPATIBLE_BACKEND_SERVICES"
_EXCEPTION_MESSAGE = "Validation failed for instance group"
responses = [
CustomOperation(
name=TEST_OPERATION_NAME,
status=CustomOperation.StatusCode.DONE,
error_code=400,
error_message="Bad request",
error=CustomOperation.LROCustomErrors(
errors=[
CustomOperation.LROCustomErrors.LROCustomError(
code=_EXCEPTION_CODE, message=_EXCEPTION_MESSAGE
)
]
),
),
]

ex_op, _, _ = make_extended_operation(responses)

# Defaults to CallError when grpc is not installed
with pytest.raises(
exceptions.BadRequest, match=f"{_EXCEPTION_CODE}: {_EXCEPTION_MESSAGE}"
):
ex_op.result()

# Inconsistent result
responses = [
CustomOperation(
Expand Down

0 comments on commit 7dfc3a7

Please sign in to comment.