Skip to content

Commit

Permalink
fix: store asset failure report supports exception without logs (#970)
Browse files Browse the repository at this point in the history
* chore: move `get_error_type`

Signed-off-by: Guilhem Barthés <[email protected]>

* feat: create `_BuildError` as base error for build error

Signed-off-by: Guilhem Barthés <[email protected]>

* doc: change fragment

Signed-off-by: Guilhem Barthés <[email protected]>

* chore: refactor `get_error_type`

Signed-off-by: Guilhem Barthés <[email protected]>

* chore: move change fragment `970.fixed` to the correct place

Signed-off-by: Guilhem Barthés <[email protected]>

---------

Signed-off-by: Guilhem Barthés <[email protected]>
  • Loading branch information
guilhem-barthes authored Aug 13, 2024
1 parent 7f70495 commit 2186fbe
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 46 deletions.
15 changes: 8 additions & 7 deletions backend/builder/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from substrapp.compute_tasks.errors import CeleryNoRetryError
from substrapp.compute_tasks.errors import CeleryRetryError
from substrapp.compute_tasks.errors import ComputeTaskErrorType
from substrapp.compute_tasks.errors import _ComputeTaskError


class PodError(Exception):
Expand All @@ -14,7 +13,7 @@ class PodTimeoutError(Exception):
pass


class BuildRetryError(_ComputeTaskError, CeleryRetryError):
class _BuildError(RuntimeError):
"""An error occurred during the build of a container image.
Args:
Expand All @@ -28,15 +27,17 @@ def __init__(self, logs: str, *args: list, **kwargs: dict):
super().__init__(logs, *args, **kwargs)


class BuildError(_ComputeTaskError, CeleryNoRetryError):
class BuildError(_BuildError, CeleryNoRetryError):
"""An error occurred during the build of a container image.
Args:
logs (str): the container image build logs
"""

error_type = ComputeTaskErrorType.BUILD_ERROR

def __init__(self, logs: str, *args: list, **kwargs: dict):
self.logs = BytesIO(str.encode(logs))
super().__init__(logs, *args, **kwargs)
class BuildRetryError(_BuildError, CeleryRetryError):
"""An error occurred during the build of a container image.
Args:
logs (str): the container image build logs
"""
18 changes: 0 additions & 18 deletions backend/substrapp/compute_tasks/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,6 @@ def __init__(self, logs: BinaryIO, *args, **kwargs):
super().__init__(logs, *args, **kwargs)


def get_error_type(exc: Exception) -> failure_report_pb2.ErrorType.ValueType:
"""From a given exception, return an error type safe to store and to advertise to the user.
Args:
exc: The exception to process.
Returns:
The error type corresponding to the exception.
"""

if isinstance(exc, _ComputeTaskError):
error_type = exc.error_type
else:
error_type = ComputeTaskErrorType.INTERNAL_ERROR

return error_type.value


class InvalidContextError(_ComputeTaskError, CeleryNoRetryError):
"""Error due to invalid task Context"""

Expand Down
28 changes: 23 additions & 5 deletions backend/substrapp/tasks/tasks_asset_failure_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from django.conf import settings

from backend.celery import app
from orchestrator import failure_report_pb2
from orchestrator import get_orchestrator_client
from substrapp.compute_tasks import errors as compute_task_errors
from substrapp.models import FailedAssetKind
from substrapp.utils.errors import store_failure

REGISTRY = settings.REGISTRY
Expand Down Expand Up @@ -45,10 +45,7 @@ def store_asset_failure_report(
) -> None:
exception = pickle.loads(exception_pickled) # nosec B301

if asset_type == FailedAssetKind.FAILED_ASSET_FUNCTION:
error_type = compute_task_errors.ComputeTaskErrorType.BUILD_ERROR.value
else:
error_type = compute_task_errors.get_error_type(exception)
error_type = get_error_type(exception)

failure_report = store_failure(exception, asset_key, asset_type, error_type)

Expand All @@ -66,3 +63,24 @@ def store_asset_failure_report(
client.register_failure_report(
{"asset_key": asset_key, "error_type": error_type, "asset_type": asset_type, "logs_address": logs_address}
)


def get_error_type(exc: Exception) -> failure_report_pb2.ErrorType.ValueType:
"""From a given exception, return an error type safe to store and to advertise to the user.
Args:
exc: The exception to process.
Returns:
The error type corresponding to the exception.
"""

if not hasattr(exc, "logs"):
return compute_task_errors.ComputeTaskErrorType.INTERNAL_ERROR.value

try:
error_type = exc.error_type # type: ignore[attr-defined]
except AttributeError:
error_type = compute_task_errors.ComputeTaskErrorType.INTERNAL_ERROR

return error_type.value
16 changes: 0 additions & 16 deletions backend/substrapp/tests/compute_tasks/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import io

import pytest

from builder import exceptions as build_errors
from orchestrator import failure_report_pb2
from substrapp.compute_tasks import errors


Expand All @@ -15,15 +11,3 @@ class TestComputeTaskErrorType:
def test_from_int(self, input_value: int, expected: errors.ComputeTaskErrorType):
result = errors.ComputeTaskErrorType.from_int(input_value)
assert result == expected


@pytest.mark.parametrize(
("exc", "expected"),
[
(build_errors.BuildError(logs="some build error"), failure_report_pb2.ERROR_TYPE_BUILD),
(errors.ExecutionError(logs=io.BytesIO()), failure_report_pb2.ERROR_TYPE_EXECUTION),
(Exception(), failure_report_pb2.ERROR_TYPE_INTERNAL),
],
)
def test_get_error_type(exc: Exception, expected: str):
assert errors.get_error_type(exc) == expected
15 changes: 15 additions & 0 deletions backend/substrapp/tests/tasks/test_store_asset_failure_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import pytest
from pytest_mock import MockerFixture

from builder import exceptions as build_errors
from orchestrator import failure_report_pb2
from substrapp.compute_tasks import errors
from substrapp.compute_tasks.errors import ComputeTaskErrorType
from substrapp.models import FailedAssetKind
from substrapp.tasks.tasks_asset_failure_report import get_error_type
from substrapp.tasks.tasks_asset_failure_report import store_asset_failure_report
from substrapp.utils.errors import store_failure

Expand Down Expand Up @@ -67,3 +70,15 @@ def test_store_failure_ignored_exception(exc_class: Type[Exception]):
)
is None
)


@pytest.mark.parametrize(
("exc", "expected"),
[
(build_errors.BuildError(logs="some build error"), failure_report_pb2.ERROR_TYPE_BUILD),
(errors.ExecutionError(logs=io.BytesIO()), failure_report_pb2.ERROR_TYPE_EXECUTION),
(Exception(), failure_report_pb2.ERROR_TYPE_INTERNAL),
],
)
def test_get_error_type(exc: Exception, expected: str):
assert get_error_type(exc) == expected
1 change: 1 addition & 0 deletions changes/970.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Celery was failing silently when a task called `FailableTask.on_failure` if the task didn't have a `logs` attribute (now return internal error)

0 comments on commit 2186fbe

Please sign in to comment.