@@ -605,6 +605,114 @@ void terminateSuspendOrchestration() throws TimeoutException, InterruptedExcepti
605605 }
606606 }
607607
608+ @ Test
609+ void rewindFailedOrchestration () throws TimeoutException {
610+ final String orchestratorName = "RewindOrchestration" ;
611+ final String activityName = "FailOnceActivity" ;
612+ final AtomicBoolean shouldFail = new AtomicBoolean (true );
613+
614+ DurableTaskGrpcWorker worker = this .createWorkerBuilder ()
615+ .addOrchestrator (orchestratorName , ctx -> {
616+ String result = ctx .callActivity (activityName , null , String .class ).await ();
617+ ctx .complete (result );
618+ })
619+ .addActivity (activityName , ctx -> {
620+ if (shouldFail .compareAndSet (true , false )) {
621+ throw new RuntimeException ("Simulated transient failure" );
622+ }
623+ return "Success after rewind" ;
624+ })
625+ .buildAndStart ();
626+
627+ DurableTaskClient client = this .createClientBuilder ().build ();
628+ try (worker ; client ) {
629+ String instanceId = client .scheduleNewOrchestrationInstance (orchestratorName );
630+
631+ // Wait for the orchestration to fail
632+ OrchestrationMetadata instance = client .waitForInstanceCompletion (instanceId , defaultTimeout , false );
633+ assertNotNull (instance );
634+ assertEquals (OrchestrationRuntimeStatus .FAILED , instance .getRuntimeStatus ());
635+
636+ // Rewind the failed orchestration with a reason
637+ String rewindReason = "Rewinding after transient failure" ;
638+ client .rewindInstance (instanceId , rewindReason );
639+
640+ // Wait for the orchestration to complete after rewind
641+ instance = client .waitForInstanceCompletion (instanceId , defaultTimeout , true );
642+ assertNotNull (instance );
643+ assertEquals (OrchestrationRuntimeStatus .COMPLETED , instance .getRuntimeStatus ());
644+ assertEquals ("Success after rewind" , instance .readOutputAs (String .class ));
645+ }
646+ }
647+
648+ @ Test
649+ void rewindFailedOrchestrationWithoutReason () throws TimeoutException {
650+ final String orchestratorName = "RewindOrchestrationNoReason" ;
651+ final String activityName = "FailOnceActivityNoReason" ;
652+ final AtomicBoolean shouldFail = new AtomicBoolean (true );
653+
654+ DurableTaskGrpcWorker worker = this .createWorkerBuilder ()
655+ .addOrchestrator (orchestratorName , ctx -> {
656+ String result = ctx .callActivity (activityName , null , String .class ).await ();
657+ ctx .complete (result );
658+ })
659+ .addActivity (activityName , ctx -> {
660+ if (shouldFail .compareAndSet (true , false )) {
661+ throw new RuntimeException ("Simulated transient failure" );
662+ }
663+ return "Success after rewind without reason" ;
664+ })
665+ .buildAndStart ();
666+
667+ DurableTaskClient client = this .createClientBuilder ().build ();
668+ try (worker ; client ) {
669+ String instanceId = client .scheduleNewOrchestrationInstance (orchestratorName );
670+
671+ // Wait for the orchestration to fail
672+ OrchestrationMetadata instance = client .waitForInstanceCompletion (instanceId , defaultTimeout , false );
673+ assertNotNull (instance );
674+ assertEquals (OrchestrationRuntimeStatus .FAILED , instance .getRuntimeStatus ());
675+
676+ // Rewind the failed orchestration without providing a reason
677+ client .rewindInstance (instanceId );
678+
679+ // Wait for the orchestration to complete after rewind
680+ instance = client .waitForInstanceCompletion (instanceId , defaultTimeout , true );
681+ assertNotNull (instance );
682+ assertEquals (OrchestrationRuntimeStatus .COMPLETED , instance .getRuntimeStatus ());
683+ assertEquals ("Success after rewind without reason" , instance .readOutputAs (String .class ));
684+ }
685+ }
686+
687+ @ Test
688+ void rewindCompletedOrchestrationThrowsException () throws TimeoutException {
689+ final String orchestratorName = "RewindCompletedOrchestration" ;
690+
691+ DurableTaskGrpcWorker worker = this .createWorkerBuilder ()
692+ .addOrchestrator (orchestratorName , ctx -> {
693+ ctx .complete ("Completed successfully" );
694+ })
695+ .buildAndStart ();
696+
697+ DurableTaskClient client = this .createClientBuilder ().build ();
698+ try (worker ; client ) {
699+ String instanceId = client .scheduleNewOrchestrationInstance (orchestratorName );
700+
701+ // Wait for the orchestration to complete
702+ OrchestrationMetadata instance = client .waitForInstanceCompletion (instanceId , defaultTimeout , true );
703+ assertNotNull (instance );
704+ assertEquals (OrchestrationRuntimeStatus .COMPLETED , instance .getRuntimeStatus ());
705+
706+ // Attempt to rewind a completed orchestration - should throw or be a no-op
707+ // Based on API behavior, rewind is only valid for FAILED orchestrations
708+ assertThrows (
709+ Exception .class ,
710+ () -> client .rewindInstance (instanceId , "Attempting to rewind completed orchestration" ),
711+ "Rewinding a completed orchestration should throw an exception"
712+ );
713+ }
714+ }
715+
608716 @ Test
609717 void activityFanOut () throws IOException , TimeoutException {
610718 final String orchestratorName = "ActivityFanOut" ;
0 commit comments