Skip to content

Commit 65188f2

Browse files
authored
Rewind API (#163)
1 parent 0faf559 commit 65188f2

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

azure/durable_functions/models/DurableOrchestrationClient.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,3 +546,57 @@ def _get_raise_event_url(
546546
request_url += "?" + "&".join(query)
547547

548548
return request_url
549+
550+
async def rewind(self,
551+
instance_id: str,
552+
reason: str,
553+
task_hub_name: Optional[str] = None,
554+
connection_name: Optional[str] = None):
555+
"""Return / "rewind" a failed orchestration instance to a prior "healthy" state.
556+
557+
Parameters
558+
----------
559+
instance_id: str
560+
The ID of the orchestration instance to rewind.
561+
reason: str
562+
The reason for rewinding the orchestration instance.
563+
task_hub_name: Optional[str]
564+
The TaskHub of the orchestration to rewind
565+
connection_name: Optional[str]
566+
Name of the application setting containing the storage
567+
connection string to use.
568+
569+
Raises
570+
------
571+
Exception:
572+
In case of a failure, it reports the reason for the exception
573+
"""
574+
request_url: str = ""
575+
if self._orchestration_bindings.rpc_base_url:
576+
path = f"instances/{instance_id}/rewind?reason={reason}"
577+
query: List[str] = []
578+
if not (task_hub_name is None):
579+
query.append(f"taskHub={task_hub_name}")
580+
if not (connection_name is None):
581+
query.append(f"connection={connection_name}")
582+
if len(query) > 0:
583+
path += "&" + "&".join(query)
584+
585+
request_url = f"{self._orchestration_bindings.rpc_base_url}" + path
586+
else:
587+
raise Exception("The Python SDK only supports RPC endpoints."
588+
+ "Please remove the `localRpcEnabled` setting from host.json")
589+
590+
response = await self._post_async_request(request_url, None)
591+
status: int = response[0]
592+
if status == 200 or status == 202:
593+
return
594+
elif status == 404:
595+
ex_msg = f"No instance with ID {instance_id} found."
596+
raise Exception(ex_msg)
597+
elif status == 410:
598+
ex_msg = "The rewind operation is only supported on failed orchestration instances."
599+
raise Exception(ex_msg)
600+
else:
601+
ex_msg = response[1]
602+
raise Exception(ex_msg)

tests/models/test_DurableOrchestrationClient.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
MESSAGE_500 = 'instance failed with unhandled exception'
2020
MESSAGE_501 = "well we didn't expect that"
2121

22+
INSTANCE_ID = "2e2568e7-a906-43bd-8364-c81733c5891e"
23+
REASON = "Stuff"
24+
2225
TEST_ORCHESTRATOR = "MyDurableOrchestrator"
2326
EXCEPTION_ORCHESTRATOR_NOT_FOUND_EXMESSAGE = "The function <orchestrator> doesn't exist,"\
2427
" is disabled, or is not an orchestrator function. Additional info: "\
@@ -540,3 +543,52 @@ async def test_start_new_orchestrator_internal_exception(binding_string):
540543
with pytest.raises(Exception) as ex:
541544
await client.start_new(TEST_ORCHESTRATOR)
542545
ex.match(status_str)
546+
547+
@pytest.mark.asyncio
548+
async def test_rewind_works_under_200_and_200_http_codes(binding_string):
549+
"""Tests that the rewind API works as expected under 'successful' http codes: 200, 202"""
550+
client = DurableOrchestrationClient(binding_string)
551+
for code in [200, 202]:
552+
mock_request = MockRequest(
553+
expected_url=f"{RPC_BASE_URL}instances/{INSTANCE_ID}/rewind?reason={REASON}",
554+
response=[code, ""])
555+
client._post_async_request = mock_request.post
556+
result = await client.rewind(INSTANCE_ID, REASON)
557+
assert result is None
558+
559+
@pytest.mark.asyncio
560+
async def test_rewind_throws_exception_during_404_410_and_500_errors(binding_string):
561+
"""Tests the behaviour of rewind under 'exception' http codes: 404, 410, 500"""
562+
client = DurableOrchestrationClient(binding_string)
563+
codes = [404, 410, 500]
564+
exception_strs = [
565+
f"No instance with ID {INSTANCE_ID} found.",
566+
"The rewind operation is only supported on failed orchestration instances.",
567+
"Something went wrong"
568+
]
569+
for http_code, expected_exception_str in zip(codes, exception_strs):
570+
mock_request = MockRequest(
571+
expected_url=f"{RPC_BASE_URL}instances/{INSTANCE_ID}/rewind?reason={REASON}",
572+
response=[http_code, "Something went wrong"])
573+
client._post_async_request = mock_request.post
574+
575+
with pytest.raises(Exception) as ex:
576+
await client.rewind(INSTANCE_ID, REASON)
577+
ex_message = str(ex.value)
578+
assert ex_message == expected_exception_str
579+
580+
@pytest.mark.asyncio
581+
async def test_rewind_with_no_rpc_endpoint(binding_string):
582+
"""Tests the behaviour of rewind without an RPC endpoint / under the legacy HTTP endpoint."""
583+
client = DurableOrchestrationClient(binding_string)
584+
mock_request = MockRequest(
585+
expected_url=f"{RPC_BASE_URL}instances/{INSTANCE_ID}/rewind?reason={REASON}",
586+
response=[-1, ""])
587+
client._post_async_request = mock_request.post
588+
client._orchestration_bindings._rpc_base_url = None
589+
expected_exception_str = "The Python SDK only supports RPC endpoints."\
590+
+ "Please remove the `localRpcEnabled` setting from host.json"
591+
with pytest.raises(Exception) as ex:
592+
await client.rewind(INSTANCE_ID, REASON)
593+
ex_message = str(ex.value)
594+
assert ex_message == expected_exception_str

0 commit comments

Comments
 (0)