File tree Expand file tree Collapse file tree
tests/azure-functions-durable Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1818
1919FIXED
2020
21+ - Fixed orchestrations failing with `OrchestrationStateError: Don't know how to
22+ handle event of type 'genericEvent'` after being rewound over the Azure
23+ Functions Durable extension. The ` genericEvent ` history event (an
24+ informational marker with no execution semantics) is now ignored during
25+ replay, matching the .NET worker, so ` rewind_orchestration ` completes the
26+ replay instead of re-failing.
2127- Fixed durabletask scheduled tasks (` durabletask.scheduled ` ) failing under
2228 data converters that reconstruct nested custom-object envelopes bottom-up
2329 (such as the Azure Functions Durable ` df ` codec). ` ScheduleState.from_json `
Original file line number Diff line number Diff line change @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
1010### Added
1111
12+ - ` DurableFunctionsClient.rewind_orchestration(...) ` (inherited from durabletask)
13+ rewinds a failed orchestration to its last known good state. The deprecated v1
14+ ` rewind(...) ` method now delegates to it instead of raising
15+ ` NotImplementedError ` .
16+
1217- ` DFApp.configure_scheduled_tasks() ` opts an app in to durabletask scheduled
1318 tasks by registering the schedule entity and operation orchestrator. Once
1419 enabled, schedules are managed from a client via
Original file line number Diff line number Diff line change @@ -393,18 +393,23 @@ async def wait_for_completion_or_create_check_status_response(
393393 return self ._create_http_response (500 , state .serialized_output )
394394 return self .create_check_status_response (request , instance_id )
395395
396- @deprecated (
397- "rewind is not yet supported in durabletask; this shim raises "
398- "NotImplementedError." )
396+ @deprecated ("rewind is deprecated; use rewind_orchestration instead." )
399397 async def rewind (
400398 self ,
401399 instance_id : str ,
402400 reason : str ,
403401 task_hub_name : Optional [str ] = None ,
404402 connection_name : Optional [str ] = None ) -> None :
405- """Not implemented: durabletask has no rewind equivalent yet."""
406- raise NotImplementedError (
407- "rewind is not yet supported by durabletask." )
403+ """Deprecated alias for :meth:`rewind_orchestration`.
404+
405+ Rewinds a failed orchestration instance to its last known good state,
406+ removing failed task and sub-orchestration results from the history and
407+ replaying from the last successful checkpoint.
408+
409+ The ``task_hub_name`` and ``connection_name`` arguments have no
410+ equivalent in durabletask and are ignored.
411+ """
412+ await self .rewind_orchestration (instance_id , reason = reason )
408413
409414 @staticmethod
410415 def _create_http_response (status_code : int , body : Union [str , Any ]) -> func .HttpResponse :
Original file line number Diff line number Diff line change @@ -2917,6 +2917,12 @@ def _cancel_timer() -> None:
29172917 elif event .HasField ("executionRewound" ):
29182918 # Informational event added when an orchestration is rewound. No action needed.
29192919 pass
2920+ elif event .HasField ("genericEvent" ):
2921+ # Informational history event with no execution semantics (for
2922+ # example, the marker the Durable Functions extension appends
2923+ # when rewinding an orchestration). Ignored during replay,
2924+ # matching the .NET worker.
2925+ pass
29202926 elif event .HasField ("eventSent" ):
29212927 # Check if this eventSent corresponds to an entity operation call after being translated to the old
29222928 # entity protocol by the Durable WebJobs extension. If so, treat this message similarly to
Original file line number Diff line number Diff line change @@ -48,3 +48,18 @@ def flaky(payload: dict) -> dict:
4848 if attempts < threshold :
4949 raise ValueError (f"flaky failure { attempts } /{ threshold } " )
5050 return {"attempts" : attempts }
51+
52+
53+ @bp .activity_trigger (input_name = "token" )
54+ def fail_once (token : str ) -> str :
55+ """Fail on the first invocation for ``token``, then succeed.
56+
57+ Used to exercise rewind: the first attempt fails the orchestration, and a
58+ rewind replays the failed activity -- which now succeeds because the
59+ in-process attempt counter has advanced.
60+ """
61+ _ATTEMPTS [token ] = _ATTEMPTS .get (token , 0 ) + 1
62+ attempts = _ATTEMPTS [token ]
63+ if attempts == 1 :
64+ raise ValueError ("failing on first attempt (rewind to retry)" )
65+ return f"succeeded on attempt { attempts } "
Original file line number Diff line number Diff line change @@ -183,10 +183,7 @@ async def wait_or_check(
183183@bp .durable_client_input (client_name = "client" )
184184async def rewind_orchestration (
185185 req : func .HttpRequest , client : df .DurableFunctionsClient ) -> func .HttpResponse :
186- try :
187- await client .rewind (req .route_params ["id" ], reason = "e2e-rewind" )
188- except NotImplementedError as exc :
189- return func .HttpResponse (str (exc ), status_code = 501 )
186+ await client .rewind (req .route_params ["id" ], reason = "e2e-rewind" )
190187 return func .HttpResponse (status_code = 202 )
191188
192189
Original file line number Diff line number Diff line change @@ -206,6 +206,14 @@ def sub_orch_fails(context: df.DurableOrchestrationContext):
206206 return result
207207
208208
209+ @bp .orchestration_trigger (context_name = "context" )
210+ def rewind_target (context : df .DurableOrchestrationContext ):
211+ # Calls an activity that fails on its first attempt. The orchestration
212+ # fails; a client rewind replays the failed activity, which then succeeds.
213+ result = yield context .call_activity ("fail_once" , context .instance_id )
214+ return result
215+
216+
209217@bp .orchestration_trigger (context_name = "context" )
210218def access_histories (context : df .DurableOrchestrationContext ):
211219 # histories is intentionally unsupported and raises NotImplementedError,
Original file line number Diff line number Diff line change @@ -104,10 +104,16 @@ def test_wait_for_completion_or_check_status(v1_app):
104104 assert result .json () == ["Hello Tokyo!" , "Hello Seattle!" , "Hello London!" ]
105105
106106
107- def test_rewind_not_implemented (v1_app ):
108- instance_id = v1_app .start_orchestration ("activity_chain" )
109- v1_app .wait_for_completion (instance_id )
107+ def test_rewind (v1_app ):
108+ # The orchestration's activity fails on its first attempt, so it lands in a
109+ # Failed state; rewinding replays the failed activity, which now succeeds.
110+ instance_id = v1_app .start_orchestration ("rewind_target" )
111+ failed = v1_app .wait_for_completion (instance_id )
112+ assert failed ["runtimeStatus" ] == "Failed"
110113
111114 result = http_request ("POST" , f"{ v1_app .base_url } /api/rewind/{ instance_id } " )
112- # rewind is a deprecated stub that raises NotImplementedError.
113- assert result .status == 501
115+ assert result .status == 202
116+
117+ status = v1_app .wait_for_status (instance_id , "Completed" )
118+ assert status ["runtimeStatus" ] == "Completed"
119+ assert status ["output" ] == "succeeded on attempt 2"
Original file line number Diff line number Diff line change @@ -332,15 +332,17 @@ async def test_wait_for_completion_returns_check_status_on_timeout():
332332
333333
334334# ---------------------------------------------------------------------------
335- # rewind (not implemented)
335+ # rewind
336336# ---------------------------------------------------------------------------
337337
338- async def test_rewind_raises_not_implemented ():
338+ async def test_rewind_delegates_to_rewind_orchestration ():
339339 client = _make_client ()
340340 try :
341- with pytest .warns (DeprecationWarning ):
342- with pytest .raises (NotImplementedError ):
341+ with patch .object (client , "rewind_orchestration" ,
342+ new = AsyncMock ()) as mock :
343+ with pytest .warns (DeprecationWarning ):
343344 await client .rewind ("abc" , "reason" )
345+ mock .assert_awaited_once_with ("abc" , reason = "reason" )
344346 finally :
345347 await client .close ()
346348
You can’t perform that action at this time.
0 commit comments