Skip to content

Commit 2b89aa3

Browse files
committed
test: cover retry task ID reuse
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e73254e2-627d-4165-8838-75369188a5a3
1 parent b229e69 commit 2b89aa3

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

tests/azure-functions-durable/test_worker_compat.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
import base64
1414
import json
1515
from concurrent.futures import ThreadPoolExecutor
16+
from datetime import datetime
1617
from types import SimpleNamespace
1718

1819
import pytest
1920

2021
import durabletask.internal.helpers as helpers
2122
import durabletask.internal.orchestrator_service_pb2 as pb
2223

24+
import azure.durable_functions as df
2325
from azure.durable_functions.worker import DurableFunctionsWorker
2426

2527
TEST_INSTANCE_ID = "inst-123"
@@ -132,6 +134,86 @@ def orchestrator(context):
132134
assert "boom" in completion.failureDetails.errorMessage
133135

134136

137+
def test_activity_retry_then_fan_out_uses_distinct_task_ids():
138+
"""Regression test for Azure/azure-functions-durable-python#603."""
139+
def orchestrator(context):
140+
options = df.RetryOptions(
141+
first_retry_interval_in_milliseconds=100,
142+
max_number_of_attempts=3,
143+
)
144+
yield context.call_activity_with_retry("flaky", options)
145+
tasks = [context.call_activity("square", value) for value in range(13)]
146+
return (yield context.task_all(tasks))
147+
148+
worker = DurableFunctionsWorker()
149+
name = "retry-then-fan-out"
150+
started_at = datetime(2026, 1, 1)
151+
152+
def execute(past_events, new_events):
153+
request = pb.OrchestratorRequest(instanceId=TEST_INSTANCE_ID)
154+
request.pastEvents.extend(past_events)
155+
request.newEvents.extend(new_events)
156+
encoded = base64.b64encode(request.SerializeToString()).decode("utf-8")
157+
return _decode_orchestrator_response(
158+
worker.execute_orchestration_request(orchestrator, encoded))
159+
160+
initial_events = [
161+
helpers.new_orchestrator_started_event(started_at),
162+
helpers.new_execution_started_event(name, TEST_INSTANCE_ID),
163+
]
164+
response = execute([], initial_events)
165+
assert len(response.actions) == 1
166+
assert response.actions[0].id == 1
167+
assert response.actions[0].scheduleTask.name == "flaky"
168+
169+
past_events = initial_events + [
170+
helpers.new_task_scheduled_event(1, "flaky"),
171+
]
172+
failure_events = [
173+
helpers.new_orchestrator_started_event(started_at),
174+
helpers.new_task_failed_event(1, ValueError("transient failure")),
175+
]
176+
response = execute(past_events, failure_events)
177+
assert len(response.actions) == 1
178+
retry_timer = response.actions[0]
179+
assert retry_timer.id == 2
180+
assert retry_timer.HasField("createTimer")
181+
182+
retry_at = retry_timer.createTimer.fireAt.ToDatetime()
183+
timer_events = [
184+
helpers.new_timer_created_event(2, retry_at),
185+
helpers.new_orchestrator_started_event(retry_at),
186+
helpers.new_timer_fired_event(2, retry_at),
187+
]
188+
past_events += failure_events
189+
response = execute(past_events, timer_events)
190+
assert len(response.actions) == 1
191+
assert response.actions[0].id == 1
192+
assert response.actions[0].scheduleTask.name == "flaky"
193+
194+
retry_completed_events = [
195+
helpers.new_orchestrator_started_event(retry_at),
196+
helpers.new_task_scheduled_event(1, "flaky"),
197+
helpers.new_task_completed_event(1, json.dumps("recovered")),
198+
]
199+
past_events += timer_events
200+
response = execute(past_events, retry_completed_events)
201+
assert [action.id for action in response.actions] == list(range(3, 16))
202+
assert all(action.scheduleTask.name == "square" for action in response.actions)
203+
204+
fan_out_events = [helpers.new_orchestrator_started_event(retry_at)]
205+
for task_id in range(3, 16):
206+
fan_out_events.append(helpers.new_task_scheduled_event(task_id, "square"))
207+
fan_out_events.append(
208+
helpers.new_task_completed_event(task_id, json.dumps(task_id - 3)))
209+
210+
past_events += retry_completed_events
211+
response = execute(past_events, fan_out_events)
212+
completion = _get_completion_action(response)
213+
assert completion.orchestrationStatus == pb.ORCHESTRATION_STATUS_COMPLETED
214+
assert json.loads(completion.result.value) == list(range(13))
215+
216+
135217
def test_execute_orchestration_request_supports_concurrent_reinvocation():
136218
def orchestrator(context):
137219
return context.instance_id

0 commit comments

Comments
 (0)