Skip to content

Commit 47d7e26

Browse files
committed
add category to application error, do not log on benign errors for activities
1 parent a2fa635 commit 47d7e26

File tree

3 files changed

+84
-0
lines changed

3 files changed

+84
-0
lines changed

temporalio/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ def __init__(
6969
self.workflow_type = workflow_type
7070
self.run_id = run_id
7171

72+
class ApplicationErrorCategory(IntEnum):
73+
"""Severity category for your application error. Maps to corresponding client-side logging/metrics behaviors"""
74+
75+
UNSPECIFIED = int(
76+
temporalio.api.enums.v1.ApplicationErrorCategory.APPLICATION_ERROR_CATEGORY_UNSPECIFIED
77+
)
78+
BENIGN = int(
79+
temporalio.api.enums.v1.ApplicationErrorCategory.APPLICATION_ERROR_CATEGORY_BENIGN
80+
)
7281

7382
class ApplicationError(FailureError):
7483
"""Error raised during workflow/activity execution."""
@@ -80,6 +89,7 @@ def __init__(
8089
type: Optional[str] = None,
8190
non_retryable: bool = False,
8291
next_retry_delay: Optional[timedelta] = None,
92+
category: ApplicationErrorCategory = ApplicationErrorCategory.UNSPECIFIED
8393
) -> None:
8494
"""Initialize an application error."""
8595
super().__init__(
@@ -91,6 +101,7 @@ def __init__(
91101
self._type = type
92102
self._non_retryable = non_retryable
93103
self._next_retry_delay = next_retry_delay
104+
self._category = category
94105

95106
@property
96107
def details(self) -> Sequence[Any]:
@@ -121,6 +132,11 @@ def next_retry_delay(self) -> Optional[timedelta]:
121132
"""
122133
return self._next_retry_delay
123134

135+
@property
136+
def category(self) -> ApplicationErrorCategory:
137+
"""Severity category of the application error"""
138+
return self._category
139+
124140

125141
class CancelledError(FailureError):
126142
"""Error raised on workflow/activity cancellation."""

temporalio/worker/_activity.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,17 @@ async def _run_activity(
490490
temporalio.exceptions.CancelledError("Cancelled"),
491491
completion.result.cancelled.failure,
492492
)
493+
elif (
494+
isinstance(
495+
err,
496+
temporalio.exceptions.ApplicationError,
497+
)
498+
and err.category == temporalio.exceptions.ApplicationErrorCategory.BENIGN
499+
):
500+
# Do not log for ApplicationError with BENIGN category.
501+
await self._data_converter.encode_failure(
502+
err, completion.result.failed.failure
503+
)
493504
else:
494505
temporalio.activity.logger.warning(
495506
"Completing activity as failed", exc_info=True

tests/worker/test_workflow.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
from temporalio.exceptions import (
8787
ActivityError,
8888
ApplicationError,
89+
ApplicationErrorCategory,
8990
CancelledError,
9091
ChildWorkflowError,
9192
TemporalError,
@@ -7421,3 +7422,59 @@ async def test_workflow_dynamic_config_failure(client: Client):
74217422
await assert_task_fail_eventually(
74227423
handle, message_contains="Dynamic config failure"
74237424
)
7425+
7426+
@activity.defn
7427+
async def raise_application_error(use_benign: bool):
7428+
if use_benign:
7429+
raise ApplicationError(
7430+
"This is a benign error",
7431+
category=ApplicationErrorCategory.BENIGN
7432+
)
7433+
else:
7434+
raise ApplicationError(
7435+
"This is a regular error",
7436+
category=ApplicationErrorCategory.UNSPECIFIED
7437+
)
7438+
7439+
7440+
@workflow.defn
7441+
class RaiseErrorWorkflow:
7442+
@workflow.run
7443+
async def run(self, use_benign: bool):
7444+
# Execute activity that will raise an error
7445+
await workflow.execute_activity(
7446+
raise_application_error,
7447+
args=[use_benign],
7448+
start_to_close_timeout=timedelta(seconds=5),
7449+
retry_policy=RetryPolicy(maximum_attempts=1)
7450+
)
7451+
7452+
7453+
async def test_activity_benign_error_not_logged(client: Client):
7454+
with LogCapturer().logs_captured(
7455+
activity.logger.base_logger
7456+
) as capturer:
7457+
async with new_worker(
7458+
client, RaiseErrorWorkflow, activities=[raise_application_error]
7459+
) as worker:
7460+
# Run with benign error
7461+
with pytest.raises(WorkflowFailureError):
7462+
await client.execute_workflow(
7463+
RaiseErrorWorkflow.run,
7464+
args=[True],
7465+
id=str(uuid.uuid4()),
7466+
task_queue=worker.task_queue,
7467+
)
7468+
7469+
assert capturer.find_log("Completing activity as failed") == None
7470+
7471+
# Run with non-benign error
7472+
with pytest.raises(WorkflowFailureError):
7473+
await client.execute_workflow(
7474+
RaiseErrorWorkflow.run,
7475+
args=[False],
7476+
id=str(uuid.uuid4()),
7477+
task_queue=worker.task_queue,
7478+
)
7479+
7480+
assert capturer.find_log("Completing activity as failed") != None

0 commit comments

Comments
 (0)