From 5aae164b82034001ceb4af30fdbf43495b2424f9 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Sun, 22 Feb 2026 14:16:51 -0800 Subject: [PATCH 1/2] initial commit --- .../models/DurableOrchestrationClient.py | 38 +++++++++ tests/conftest.py | 2 + .../models/test_DurableOrchestrationClient.py | 78 ++++++++++++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/azure/durable_functions/models/DurableOrchestrationClient.py b/azure/durable_functions/models/DurableOrchestrationClient.py index cc746cd2..73629e53 100644 --- a/azure/durable_functions/models/DurableOrchestrationClient.py +++ b/azure/durable_functions/models/DurableOrchestrationClient.py @@ -767,6 +767,44 @@ async def suspend(self, instance_id: str, reason: str) -> None: if error_message: raise Exception(error_message) + async def restart(self, instance_id: str, + restart_with_new_instance_id: bool = True) -> str: + """Restart an orchestration instance with its original input. + + Parameters + ---------- + instance_id : str + The ID of the orchestration instance to restart. + restart_with_new_instance_id : bool + If True, the restarted instance will use a new instance ID. + If False, the restarted instance will reuse the original instance ID. + + Raises + ------ + Exception: + When the instance with the given ID is not found. + + Returns + ------- + str + The instance ID of the restarted orchestration. + """ + status = await self.get_status(instance_id, show_input=True) + + if not status or status.name is None: + raise Exception( + f"An orchestration with the instanceId {instance_id} was not found.") + + if restart_with_new_instance_id: + return await self.start_new( + orchestration_function_name=status.name, + client_input=status.input_) + else: + return await self.start_new( + orchestration_function_name=status.name, + instance_id=status.instance_id, + client_input=status.input_) + async def resume(self, instance_id: str, reason: str) -> None: """Resume the specified orchestration instance. diff --git a/tests/conftest.py b/tests/conftest.py index ca65ee23..68a8f683 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,8 @@ def get_binding_string(): "resumePostUri": f"{BASE_URL}/instances/INSTANCEID/resume?reason=" "{text}&taskHub=" f"{TASK_HUB_NAME}&connection=Storage&code={AUTH_CODE}", + "restartPostUri": f"{BASE_URL}/instances/INSTANCEID/restart?taskHub=" + f"{TASK_HUB_NAME}&connection=Storage&code={AUTH_CODE}", }, "rpcBaseUrl": RPC_BASE_URL } diff --git a/tests/models/test_DurableOrchestrationClient.py b/tests/models/test_DurableOrchestrationClient.py index 7707a63a..0e8290c3 100644 --- a/tests/models/test_DurableOrchestrationClient.py +++ b/tests/models/test_DurableOrchestrationClient.py @@ -156,7 +156,11 @@ def test_create_check_status_response(binding_string): "resumePostUri": r"http://test_azure.net/runtime/webhooks/durabletask/instances/" r"2e2568e7-a906-43bd-8364-c81733c5891e/resume" - r"?reason={text}&taskHub=TASK_HUB_NAME&connection=Storage&code=AUTH_CODE" + r"?reason={text}&taskHub=TASK_HUB_NAME&connection=Storage&code=AUTH_CODE", + "restartPostUri": + r"http://test_azure.net/runtime/webhooks/durabletask/instances/" + r"2e2568e7-a906-43bd-8364-c81733c5891e/restart" + r"?taskHub=TASK_HUB_NAME&connection=Storage&code=AUTH_CODE" } for key, _ in http_management_payload.items(): http_management_payload[key] = replace_stand_in_bits(http_management_payload[key]) @@ -739,3 +743,75 @@ async def test_post_500_resume(binding_string): with pytest.raises(Exception): await client.resume(TEST_INSTANCE_ID, raw_reason) + + +@pytest.mark.asyncio +async def test_restart_with_new_instance_id(binding_string): + """Test restart creates a new instance with a new ID by default.""" + orchestrator_name = "MyOrchestrator" + original_input = {"key": "value"} + new_instance_id = "new-instance-id-1234" + + get_mock = MockRequest( + expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}?showInput=True", + response=[200, dict( + name=orchestrator_name, + instanceId=TEST_INSTANCE_ID, + createdTime=TEST_CREATED_TIME, + lastUpdatedTime=TEST_LAST_UPDATED_TIME, + runtimeStatus="Completed", + input=original_input)]) + + post_mock = MockRequest( + expected_url=f"{RPC_BASE_URL}orchestrators/{orchestrator_name}", + response=[202, {"id": new_instance_id}]) + + client = DurableOrchestrationClient(binding_string) + client._get_async_request = get_mock.get + client._post_async_request = post_mock.post + + result = await client.restart(TEST_INSTANCE_ID) + assert result == new_instance_id + + +@pytest.mark.asyncio +async def test_restart_with_same_instance_id(binding_string): + """Test restart reuses the original instance ID when restartWithNewInstanceId is False.""" + orchestrator_name = "MyOrchestrator" + original_input = {"key": "value"} + + get_mock = MockRequest( + expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}?showInput=True", + response=[200, dict( + name=orchestrator_name, + instanceId=TEST_INSTANCE_ID, + createdTime=TEST_CREATED_TIME, + lastUpdatedTime=TEST_LAST_UPDATED_TIME, + runtimeStatus="Completed", + input=original_input)]) + + post_mock = MockRequest( + expected_url=f"{RPC_BASE_URL}orchestrators/{orchestrator_name}/{TEST_INSTANCE_ID}", + response=[202, {"id": TEST_INSTANCE_ID}]) + + client = DurableOrchestrationClient(binding_string) + client._get_async_request = get_mock.get + client._post_async_request = post_mock.post + + result = await client.restart(TEST_INSTANCE_ID, restart_with_new_instance_id=False) + assert result == TEST_INSTANCE_ID + + +@pytest.mark.asyncio +async def test_restart_instance_not_found(binding_string): + """Test restart raises exception when instance is not found.""" + get_mock = MockRequest( + expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}?showInput=True", + response=[404, dict(createdTime=None, lastUpdatedTime=None)]) + + client = DurableOrchestrationClient(binding_string) + client._get_async_request = get_mock.get + + with pytest.raises(Exception) as ex: + await client.restart(TEST_INSTANCE_ID) + assert f"instanceId {TEST_INSTANCE_ID} was not found" in str(ex.value) From 9348922887fbd4d092018746047ebedbeab4706c Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 24 Feb 2026 20:07:20 -0800 Subject: [PATCH 2/2] udpate with comments --- .../models/DurableOrchestrationClient.py | 32 +++++++----- .../models/test_DurableOrchestrationClient.py | 49 +++++-------------- 2 files changed, 30 insertions(+), 51 deletions(-) diff --git a/azure/durable_functions/models/DurableOrchestrationClient.py b/azure/durable_functions/models/DurableOrchestrationClient.py index 42dfc76a..009001e5 100644 --- a/azure/durable_functions/models/DurableOrchestrationClient.py +++ b/azure/durable_functions/models/DurableOrchestrationClient.py @@ -813,21 +813,27 @@ async def restart(self, instance_id: str, str The instance ID of the restarted orchestration. """ - status = await self.get_status(instance_id, show_input=True) + restart_with_new_instance_id_str = str(restart_with_new_instance_id).lower() + request_url = f"{self._orchestration_bindings.rpc_base_url}instances/{instance_id}/" \ + f"restart?restartWithNewInstanceId={restart_with_new_instance_id_str}" + response = await self._post_async_request( + request_url, + None, + function_invocation_id=self._function_invocation_id) + switch_statement = { + 202: lambda: None, # instance is restarted + 410: lambda: None, # instance completed + 404: lambda: f"No instance with ID '{instance_id}' found.", + } - if not status or status.name is None: - raise Exception( - f"An orchestration with the instanceId {instance_id} was not found.") + has_error_message = switch_statement.get( + response[0], + lambda: f"The operation failed with an unexpected status code {response[0]}") + error_message = has_error_message() + if error_message: + raise Exception(error_message) - if restart_with_new_instance_id: - return await self.start_new( - orchestration_function_name=status.name, - client_input=status.input_) - else: - return await self.start_new( - orchestration_function_name=status.name, - instance_id=status.instance_id, - client_input=status.input_) + return response[1] if response[1] else instance_id async def resume(self, instance_id: str, reason: str) -> None: """Resume the specified orchestration instance. diff --git a/tests/models/test_DurableOrchestrationClient.py b/tests/models/test_DurableOrchestrationClient.py index 747adfe4..1466587c 100644 --- a/tests/models/test_DurableOrchestrationClient.py +++ b/tests/models/test_DurableOrchestrationClient.py @@ -748,27 +748,14 @@ async def test_post_500_resume(binding_string): @pytest.mark.asyncio async def test_restart_with_new_instance_id(binding_string): - """Test restart creates a new instance with a new ID by default.""" - orchestrator_name = "MyOrchestrator" - original_input = {"key": "value"} + """Test restart calls the HTTP restart endpoint with restartWithNewInstanceId=true.""" new_instance_id = "new-instance-id-1234" - get_mock = MockRequest( - expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}?showInput=True", - response=[200, dict( - name=orchestrator_name, - instanceId=TEST_INSTANCE_ID, - createdTime=TEST_CREATED_TIME, - lastUpdatedTime=TEST_LAST_UPDATED_TIME, - runtimeStatus="Completed", - input=original_input)]) - post_mock = MockRequest( - expected_url=f"{RPC_BASE_URL}orchestrators/{orchestrator_name}", - response=[202, {"id": new_instance_id}]) + expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}/restart?restartWithNewInstanceId=true", + response=[202, new_instance_id]) client = DurableOrchestrationClient(binding_string) - client._get_async_request = get_mock.get client._post_async_request = post_mock.post result = await client.restart(TEST_INSTANCE_ID) @@ -777,26 +764,12 @@ async def test_restart_with_new_instance_id(binding_string): @pytest.mark.asyncio async def test_restart_with_same_instance_id(binding_string): - """Test restart reuses the original instance ID when restartWithNewInstanceId is False.""" - orchestrator_name = "MyOrchestrator" - original_input = {"key": "value"} - - get_mock = MockRequest( - expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}?showInput=True", - response=[200, dict( - name=orchestrator_name, - instanceId=TEST_INSTANCE_ID, - createdTime=TEST_CREATED_TIME, - lastUpdatedTime=TEST_LAST_UPDATED_TIME, - runtimeStatus="Completed", - input=original_input)]) - + """Test restart calls the HTTP restart endpoint with restartWithNewInstanceId=false.""" post_mock = MockRequest( - expected_url=f"{RPC_BASE_URL}orchestrators/{orchestrator_name}/{TEST_INSTANCE_ID}", - response=[202, {"id": TEST_INSTANCE_ID}]) + expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}/restart?restartWithNewInstanceId=false", + response=[202, TEST_INSTANCE_ID]) client = DurableOrchestrationClient(binding_string) - client._get_async_request = get_mock.get client._post_async_request = post_mock.post result = await client.restart(TEST_INSTANCE_ID, restart_with_new_instance_id=False) @@ -806,16 +779,16 @@ async def test_restart_with_same_instance_id(binding_string): @pytest.mark.asyncio async def test_restart_instance_not_found(binding_string): """Test restart raises exception when instance is not found.""" - get_mock = MockRequest( - expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}?showInput=True", - response=[404, dict(createdTime=None, lastUpdatedTime=None)]) + post_mock = MockRequest( + expected_url=f"{RPC_BASE_URL}instances/{TEST_INSTANCE_ID}/restart?restartWithNewInstanceId=true", + response=[404, None]) client = DurableOrchestrationClient(binding_string) - client._get_async_request = get_mock.get + client._post_async_request = post_mock.post with pytest.raises(Exception) as ex: await client.restart(TEST_INSTANCE_ID) - assert f"instanceId {TEST_INSTANCE_ID} was not found" in str(ex.value) + assert f"No instance with ID '{TEST_INSTANCE_ID}' found." in str(ex.value) # Tests for function_invocation_id parameter def test_client_stores_function_invocation_id(binding_string): """Test that the client stores the function_invocation_id parameter."""