Skip to content

Commit 5926b76

Browse files
authored
Minor exception-related updates (#75)
1 parent e5bd9a9 commit 5926b76

File tree

5 files changed

+102
-17
lines changed

5 files changed

+102
-17
lines changed

temporalio/exceptions.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import traceback
44
from enum import IntEnum
5-
from typing import Any, Awaitable, Callable, Iterable, Optional
5+
from typing import Any, Awaitable, Callable, Iterable, Optional, Tuple
66

77
import temporalio.api.common.v1
88
import temporalio.api.enums.v1
@@ -39,10 +39,13 @@ def __init__(
3939
self,
4040
message: str,
4141
*,
42-
failure: Optional[temporalio.api.failure.v1.Failure] = None
42+
failure: Optional[temporalio.api.failure.v1.Failure] = None,
43+
exc_args: Optional[Tuple] = None,
4344
) -> None:
4445
"""Initialize a failure error."""
45-
super().__init__(message)
46+
if exc_args is None:
47+
exc_args = (message,)
48+
super().__init__(*exc_args)
4649
self._message = message
4750
self._failure = failure
4851

@@ -65,10 +68,14 @@ def __init__(
6568
message: str,
6669
*details: Any,
6770
type: Optional[str] = None,
68-
non_retryable: bool = False
71+
non_retryable: bool = False,
6972
) -> None:
7073
"""Initialize an application error."""
71-
super().__init__(message)
74+
super().__init__(
75+
message,
76+
# If there is a type, prepend it to the message on the string repr
77+
exc_args=(message if not type else f"{type}: {message}",),
78+
)
7279
self._details = details
7380
self._type = type
7481
self._non_retryable = non_retryable
@@ -140,7 +147,7 @@ def __init__(
140147
message: str,
141148
*,
142149
type: Optional[TimeoutType],
143-
last_heartbeat_details: Iterable[Any]
150+
last_heartbeat_details: Iterable[Any],
144151
) -> None:
145152
"""Initialize a timeout error."""
146153
super().__init__(message)
@@ -206,7 +213,7 @@ def __init__(
206213
identity: str,
207214
activity_type: str,
208215
activity_id: str,
209-
retry_state: Optional[RetryState]
216+
retry_state: Optional[RetryState],
210217
) -> None:
211218
"""Initialize an activity error."""
212219
super().__init__(message)
@@ -261,7 +268,7 @@ def __init__(
261268
workflow_type: str,
262269
initiated_event_id: int,
263270
started_event_id: int,
264-
retry_state: Optional[RetryState]
271+
retry_state: Optional[RetryState],
265272
) -> None:
266273
"""Initialize a child workflow error."""
267274
super().__init__(message)
@@ -404,7 +411,7 @@ def apply_error_to_failure(
404411

405412
# Set message, stack, and cause. Obtaining cause follows rules from
406413
# https://docs.python.org/3/library/exceptions.html#exception-context
407-
failure.message = str(error)
414+
failure.message = error.message
408415
if error.__traceback__:
409416
failure.stack_trace = "\n".join(traceback.format_tb(error.__traceback__))
410417
if error.__cause__:
@@ -490,6 +497,7 @@ def apply_exception_to_failure(
490497
str(exception), type=exception.__class__.__name__
491498
)
492499
failure_error.__traceback__ = exception.__traceback__
500+
failure_error.__cause__ = exception.__cause__
493501
apply_error_to_failure(failure_error, converter, failure)
494502

495503

temporalio/worker/activity.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ async def _run_activity(
418418
and running_activity.cancelled_due_to_heartbeat_error
419419
):
420420
err = running_activity.cancelled_due_to_heartbeat_error
421-
temporalio.activity.logger.debug(
421+
temporalio.activity.logger.warning(
422422
f"Completing as failure during heartbeat with error of type {type(err)}: {err}",
423423
)
424424
await temporalio.exceptions.encode_exception_to_failure(
@@ -438,8 +438,8 @@ async def _run_activity(
438438
completion.result.cancelled.failure,
439439
)
440440
else:
441-
temporalio.activity.logger.debug(
442-
"Completing as failed", exc_info=True
441+
temporalio.activity.logger.warning(
442+
"Completing activity as failed", exc_info=True
443443
)
444444
await temporalio.exceptions.encode_exception_to_failure(
445445
err,

temporalio/worker/workflow_instance.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,10 @@ def activate(
265265
# Run one iteration of the loop
266266
self._run_once()
267267
except Exception as err:
268-
logger.exception(f"Failed activation on workflow with run ID {act.run_id}")
268+
logger.warning(
269+
f"Failed activation on workflow {self._info.workflow_type} with ID {self._info.workflow_id} and run ID {self._info.run_id}",
270+
exc_info=True,
271+
)
269272
# Set completion failure
270273
self._current_completion.failed.failure.SetInParent()
271274
try:

tests/test_exceptions.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import logging
2+
import logging.handlers
3+
import traceback
4+
from typing import Optional
5+
6+
import temporalio.converter
7+
from temporalio.api.failure.v1 import Failure
8+
from temporalio.exceptions import (
9+
ApplicationError,
10+
FailureError,
11+
apply_exception_to_failure,
12+
failure_to_error,
13+
)
14+
15+
16+
# This is an example of appending the stack to every Temporal failure error
17+
def append_temporal_stack(exc: Optional[BaseException]) -> None:
18+
while exc:
19+
# Only append if it doesn't appear already there
20+
if (
21+
isinstance(exc, FailureError)
22+
and exc.failure
23+
and exc.failure.stack_trace
24+
and len(exc.args) == 1
25+
and "\nStack:\n" not in exc.args[0]
26+
):
27+
exc.args = (f"{exc}\nStack:\n{exc.failure.stack_trace.rstrip()}",)
28+
exc = exc.__cause__
29+
30+
31+
def test_exception_format():
32+
# Cause a nested exception
33+
actual_err: Exception
34+
try:
35+
try:
36+
raise ValueError("error1")
37+
except Exception as err:
38+
raise RuntimeError("error2") from err
39+
except Exception as err:
40+
actual_err = err
41+
assert actual_err
42+
43+
# Convert to failure and back
44+
failure = Failure()
45+
apply_exception_to_failure(
46+
actual_err, temporalio.converter.default().payload_converter, failure
47+
)
48+
failure_error = failure_to_error(
49+
failure, temporalio.converter.default().payload_converter
50+
)
51+
# Confirm type is prepended
52+
assert isinstance(failure_error, ApplicationError)
53+
assert "RuntimeError: error2" == str(failure_error)
54+
assert isinstance(failure_error.cause, ApplicationError)
55+
assert "ValueError: error1" == str(failure_error.cause)
56+
57+
# Append the stack and format the exception and check the output
58+
append_temporal_stack(failure_error)
59+
output = "".join(
60+
traceback.format_exception(
61+
type(failure_error), failure_error, failure_error.__traceback__
62+
)
63+
)
64+
assert "temporalio.exceptions.ApplicationError: ValueError: error1" in output
65+
assert "temporalio.exceptions.ApplicationError: RuntimeError: error" in output
66+
assert output.count("\nStack:\n") == 2
67+
68+
# This shows how it might look for those with debugging on
69+
logging.getLogger(__name__).debug(
70+
"Showing appended exception", exc_info=failure_error
71+
)

tests/worker/test_activity.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ async def raise_error():
172172

173173
with pytest.raises(WorkflowFailureError) as err:
174174
await _execute_workflow_with_activity(client, worker, raise_error)
175-
assert str(assert_activity_application_error(err.value)) == "oh no!"
175+
assert str(assert_activity_application_error(err.value)) == "RuntimeError: oh no!"
176176

177177

178178
@activity.defn
@@ -189,7 +189,7 @@ async def test_sync_activity_process_failure(client: Client, worker: ExternalWor
189189
picklable_activity_failure,
190190
worker_config={"activity_executor": executor},
191191
)
192-
assert str(assert_activity_application_error(err.value)) == "oh no!"
192+
assert str(assert_activity_application_error(err.value)) == "RuntimeError: oh no!"
193193

194194

195195
async def test_activity_bad_params(client: Client, worker: ExternalWorker):
@@ -328,7 +328,7 @@ async def say_hello(name: str) -> str:
328328
task_queue=worker.task_queue,
329329
)
330330
assert str(assert_activity_application_error(err.value)) == (
331-
"Activity function wrong_activity is not registered on this worker, "
331+
"NotFoundError: Activity function wrong_activity is not registered on this worker, "
332332
"available activities: say_hello"
333333
)
334334

@@ -565,7 +565,10 @@ async def some_activity():
565565
retry_max_attempts=100,
566566
non_retryable_error_types=["Cannot retry me"],
567567
)
568-
assert str(assert_activity_application_error(err.value)) == "Do not retry me"
568+
assert (
569+
str(assert_activity_application_error(err.value))
570+
== "Cannot retry me: Do not retry me"
571+
)
569572

570573

571574
async def test_activity_logging(client: Client, worker: ExternalWorker):

0 commit comments

Comments
 (0)