Skip to content

Commit 6c25a78

Browse files
authored
Implemented out-of-proc error reporting schema (#196)
1 parent 65188f2 commit 6c25a78

7 files changed

+152
-60
lines changed

azure/durable_functions/orchestrator.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,22 @@ def handle(self, context: DurableOrchestrationContext):
100100
actions=self.durable_context.actions,
101101
custom_status=self.durable_context.custom_status)
102102
except Exception as e:
103+
exception_str = str(e)
103104
orchestration_state = OrchestratorState(
104105
is_done=False,
105106
output=None, # Should have no output, after generation range
106107
actions=self.durable_context.actions,
107-
error=str(e),
108+
error=exception_str,
108109
custom_status=self.durable_context.custom_status)
109110

111+
# Create formatted error, using out-of-proc error schema
112+
error_label = "\n\n$OutOfProcData$:"
113+
state_str = orchestration_state.to_json_string()
114+
formatted_error = f"{exception_str}{error_label}{state_str}"
115+
116+
# Raise exception, re-set stack to original location
117+
raise Exception(formatted_error) from e
118+
110119
# No output if continue_as_new was called
111120
if self.durable_context.will_continue_as_new:
112121
orchestration_state._output = None

tests/orchestrator/test_call_http.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,25 @@ def test_failed_state():
104104
add_failed_http_events(
105105
context_builder, 0, failed_reason, failed_details)
106106

107-
result = get_orchestration_state_result(
108-
context_builder, simple_get_generator_function)
109-
110-
expected_state = base_expected_state()
111-
request = get_request()
112-
add_http_action(expected_state, request)
113-
expected_state._error = f'{failed_reason} \n {failed_details}'
114-
expected = expected_state.to_json()
115-
116-
assert_valid_schema(result)
117-
assert_orchestration_state_equals(expected, result)
107+
try:
108+
result = get_orchestration_state_result(
109+
context_builder, simple_get_generator_function)
110+
# We expected an exception
111+
assert False
112+
except Exception as e:
113+
error_label = "\n\n$OutOfProcData$:"
114+
error_str = str(e)
115+
116+
expected_state = base_expected_state()
117+
request = get_request()
118+
add_http_action(expected_state, request)
119+
120+
error_msg = f'{failed_reason} \n {failed_details}'
121+
expected_state._error = error_msg
122+
state_str = expected_state.to_json_string()
123+
124+
expected_error_str = f"{error_msg}{error_label}{state_str}"
125+
assert expected_error_str == error_str
118126

119127

120128
def test_initial_post_state():

tests/orchestrator/test_fan_out_fan_in.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,22 @@ def test_failed_parrot_value():
153153
add_completed_task_set_events(context_builder, 1, 'ParrotValue', activity_count,
154154
2, failed_reason, failed_details)
155155

156-
result = get_orchestration_state_result(
157-
context_builder, generator_function)
158-
159-
expected_state = base_expected_state(error=f'{failed_reason} \n {failed_details}')
160-
add_single_action(expected_state, function_name='GetActivityCount', input_=None)
161-
add_multi_actions(expected_state, function_name='ParrotValue', volume=activity_count)
162-
expected = expected_state.to_json()
163-
164-
assert_valid_schema(result)
165-
assert_orchestration_state_equals(expected, result)
156+
try:
157+
result = get_orchestration_state_result(
158+
context_builder, generator_function)
159+
# we expected an exception
160+
assert False
161+
except Exception as e:
162+
error_label = "\n\n$OutOfProcData$:"
163+
error_str = str(e)
164+
165+
expected_state = base_expected_state(error=f'{failed_reason} \n {failed_details}')
166+
add_single_action(expected_state, function_name='GetActivityCount', input_=None)
167+
add_multi_actions(expected_state, function_name='ParrotValue', volume=activity_count)
168+
169+
error_msg = f'{failed_reason} \n {failed_details}'
170+
expected_state._error = error_msg
171+
state_str = expected_state.to_json_string()
172+
173+
expected_error_str = f"{error_msg}{error_label}{state_str}"
174+
assert expected_error_str == error_str

tests/orchestrator/test_retries.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,16 @@ def test_retries_can_fail():
255255
"""Tests the code path where a retry'ed Task fails"""
256256
context = get_context_with_retries(will_fail=True)
257257

258-
result = get_orchestration_state_result(
259-
context, generator_function)
260-
261-
expected_error = f"{REASONS} \n {DETAILS}"
262-
assert "error" in result
263-
assert result["error"] == expected_error
258+
try:
259+
result = get_orchestration_state_result(
260+
context, generator_function)
261+
# We expected an exception
262+
assert False
263+
except Exception as e:
264+
error_label = "\n\n$OutOfProcData$:"
265+
error_str = str(e)
266+
267+
error_msg = f"{REASONS} \n {DETAILS}"
268+
269+
expected_error_str = f"{error_msg}{error_label}"
270+
assert str.startswith(error_str, expected_error_str)

tests/orchestrator/test_sequential_orchestrator.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ def generator_function(context):
2020

2121
return outputs
2222

23+
def generator_function_rasing_ex(context):
24+
outputs = []
25+
26+
task1 = yield context.call_activity("Hello", "Tokyo")
27+
task2 = yield context.call_activity("Hello", "Seattle")
28+
task3 = yield context.call_activity("Hello", "London")
29+
30+
outputs.append(task1)
31+
outputs.append(task2)
32+
outputs.append(task3)
33+
34+
raise ValueError("Oops!")
2335

2436
def generator_function_with_serialization(context):
2537
"""Ochestrator to test sequential activity calls with a serializable input arguments."""
@@ -99,17 +111,50 @@ def test_failed_tokyo_state():
99111
add_hello_failed_events(
100112
context_builder, 0, failed_reason, failed_details)
101113

102-
result = get_orchestration_state_result(
103-
context_builder, generator_function)
104-
105-
expected_state = base_expected_state()
106-
add_hello_action(expected_state, 'Tokyo')
107-
expected_state._error = f'{failed_reason} \n {failed_details}'
108-
expected = expected_state.to_json()
109-
110-
assert_valid_schema(result)
111-
assert_orchestration_state_equals(expected, result)
114+
try:
115+
result = get_orchestration_state_result(
116+
context_builder, generator_function)
117+
# expected an exception
118+
assert False
119+
except Exception as e:
120+
error_label = "\n\n$OutOfProcData$:"
121+
error_str = str(e)
122+
123+
expected_state = base_expected_state()
124+
add_hello_action(expected_state, 'Tokyo')
125+
error_msg = f'{failed_reason} \n {failed_details}'
126+
expected_state._error = error_msg
127+
state_str = expected_state.to_json_string()
128+
129+
expected_error_str = f"{error_msg}{error_label}{state_str}"
130+
assert expected_error_str == error_str
131+
132+
133+
def test_user_code_raises_exception():
134+
context_builder = ContextBuilder('test_simple_function')
135+
add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
136+
add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
137+
add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
112138

139+
try:
140+
result = get_orchestration_state_result(
141+
context_builder, generator_function_rasing_ex)
142+
# expected an exception
143+
assert False
144+
except Exception as e:
145+
error_label = "\n\n$OutOfProcData$:"
146+
error_str = str(e)
147+
148+
expected_state = base_expected_state()
149+
add_hello_action(expected_state, 'Tokyo')
150+
add_hello_action(expected_state, 'Seattle')
151+
add_hello_action(expected_state, 'London')
152+
error_msg = 'Oops!'
153+
expected_state._error = error_msg
154+
state_str = expected_state.to_json_string()
155+
156+
expected_error_str = f"{error_msg}{error_label}{state_str}"
157+
assert expected_error_str == error_str
113158

114159
def test_tokyo_and_seattle_state():
115160
context_builder = ContextBuilder('test_simple_function')

tests/orchestrator/test_sequential_orchestrator_with_retry.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,21 @@ def test_failed_tokyo_hit_max_attempts():
198198
add_hello_failed_events(context_builder, 4, failed_reason, failed_details)
199199
add_retry_timer_events(context_builder, 5)
200200

201-
result = get_orchestration_state_result(
202-
context_builder, generator_function)
203-
204-
expected_state = base_expected_state()
205-
add_hello_action(expected_state, 'Tokyo')
206-
expected_state._error = f'{failed_reason} \n {failed_details}'
207-
expected = expected_state.to_json()
208-
209-
assert_valid_schema(result)
210-
assert_orchestration_state_equals(expected, result)
201+
try:
202+
result = get_orchestration_state_result(
203+
context_builder, generator_function)
204+
# expected an exception
205+
assert False
206+
except Exception as e:
207+
error_label = "\n\n$OutOfProcData$:"
208+
error_str = str(e)
209+
210+
expected_state = base_expected_state()
211+
add_hello_action(expected_state, 'Tokyo')
212+
213+
error_msg = f'{failed_reason} \n {failed_details}'
214+
expected_state._error = error_msg
215+
state_str = expected_state.to_json_string()
216+
217+
expected_error_str = f"{error_msg}{error_label}{state_str}"
218+
assert expected_error_str == error_str

tests/orchestrator/test_sub_orchestrator_with_retry.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,21 @@ def test_tokyo_and_seattle_and_london_state_all_failed():
109109
add_hello_suborch_failed_events(context_builder, 4, failed_reason, failed_details)
110110
add_retry_timer_events(context_builder, 5)
111111

112-
113-
result = get_orchestration_state_result(
114-
context_builder, generator_function)
115-
116-
expected_state = base_expected_state()
117-
add_hello_suborch_action(expected_state, 'Tokyo')
118-
expected_state._error = f'{failed_reason} \n {failed_details}'
119-
expected = expected_state.to_json()
120-
expected_state._is_done = True
121-
122-
#assert_valid_schema(result)
123-
assert_orchestration_state_equals(expected, result)
112+
try:
113+
result = get_orchestration_state_result(
114+
context_builder, generator_function)
115+
# Should have error'ed out
116+
assert False
117+
except Exception as e:
118+
error_label = "\n\n$OutOfProcData$:"
119+
error_str = str(e)
120+
121+
expected_state = base_expected_state()
122+
add_hello_suborch_action(expected_state, 'Tokyo')
123+
124+
error_msg = f'{failed_reason} \n {failed_details}'
125+
expected_state._error = error_msg
126+
state_str = expected_state.to_json_string()
127+
128+
expected_error_str = f"{error_msg}{error_label}{state_str}"
129+
assert expected_error_str == error_str

0 commit comments

Comments
 (0)