@@ -310,3 +310,77 @@ def new_say_hello_worker(client: Client) -> Worker:
310310 workflows = [SayHelloWorkflow ],
311311 activities = [say_hello ],
312312 )
313+
314+
315+ @workflow .defn
316+ class UpdateCompletionAfterWorkflowReturn :
317+ def __init__ (self ) -> None :
318+ self .workflow_returned = False
319+
320+ @workflow .run
321+ async def run (self ) -> str :
322+ self .workflow_returned = True
323+ return "workflow-result"
324+
325+ @workflow .update
326+ async def my_update (self ) -> str :
327+ await workflow .wait_condition (lambda : self .workflow_returned )
328+ return "update-result"
329+
330+
331+ async def test_replayer_command_reordering_backward_compatibility () -> None :
332+ """
333+ The UpdateCompletionAfterWorkflowReturn workflow above features an update handler that returns
334+ after the main workflow coroutine has exited. It will (if an update is sent in the first WFT)
335+ generate a raw command sequence (before sending to core) of
336+
337+ [UpdateAccepted, CompleteWorkflowExecution, UpdateCompleted].
338+
339+ Prior to https://github.com/temporalio/sdk-python/pull/569, Python truncated this command
340+ sequence to
341+
342+ [UpdateAccepted, CompleteWorkflowExecution].
343+
344+ With #569, Python performs no truncation, and Core changes it to
345+
346+ [UpdateAccepted, UpdateCompleted, CompleteWorkflowExecution].
347+
348+ This test takes a history generated using pre-#569 SDK code, and replays it. This succeeds.
349+ The history is
350+
351+ 1 WorkflowExecutionStarted
352+ 2 WorkflowTaskScheduled
353+ 3 WorkflowTaskStarted
354+ 4 WorkflowTaskCompleted
355+ 5 WorkflowExecutionUpdateAccepted
356+ 6 WorkflowExecutionCompleted
357+
358+ Note that the history lacks a WorkflowExecutionUpdateCompleted event.
359+
360+ If Core's logic (which involves a flag) incorrectly allowed this history to be replayed
361+ using Core's post-#569 implementation, then a non-determinism error would result. Specifically,
362+ Core would, at some point during replay, do the following:
363+
364+ Receive [UpdateAccepted, CompleteWorkflowExecution, UpdateCompleted] from lang,
365+ change that to [UpdateAccepted, UpdateCompleted, CompleteWorkflowExecution]
366+ and create an UpdateMachine instance (the WorkflowTaskMachine instance already exists).
367+ Then continue to consume history events.
368+
369+ Event 5 WorkflowExecutionUpdateAccepted applies to the UpdateMachine associated with
370+ the UpdateAccepted command, but event 6 WorkflowExecutionCompleted does not, since
371+ core is expecting an event that can be applied to the UpdateMachine corresponding to
372+ UpdateCompleted. If we modify core to incorrectly apply its new logic then we do see that:
373+
374+ [TMPRL1100] Nondeterminism error: Update machine does not handle this event: HistoryEvent(id: 6, WorkflowExecutionCompleted)
375+
376+ The test passes because core in fact (because the history lacks the flag) uses its old logic
377+ and changes the command sequence from [UpdateAccepted, CompleteWorkflowExecution, UpdateCompleted]
378+ to [UpdateAccepted, CompleteWorkflowExecution], thus events 5 and 6 match.
379+ """
380+ with Path (__file__ ).with_name (
381+ "test_replayer_command_reordering_backward_compatibility.json"
382+ ).open () as f :
383+ history = f .read ()
384+ await Replayer (workflows = [UpdateCompletionAfterWorkflowReturn ]).replay_workflow (
385+ WorkflowHistory .from_json ("fake" , history )
386+ )
0 commit comments