diff --git a/src/Test/TestCases.Runtime/StatementsBehaviorExtensionTests.cs b/src/Test/TestCases.Runtime/StatementsBehaviorExtensionTests.cs new file mode 100644 index 00000000..0db6caaa --- /dev/null +++ b/src/Test/TestCases.Runtime/StatementsBehaviorExtensionTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Activities; +using System.Activities.Statements; +using System.Threading; +using Shouldly; +using Test.Common.TestObjects.CustomActivities; +using UiPath.Workflow.Runtime.Statements; +using WorkflowApplicationTestExtensions.Persistence; +using Xunit; + +namespace TestCases.Runtime +{ + public class StatementsBehaviorExtensionTests + { + /// + /// Tests that using StatementsBehaviorExtension extension with BlockingDelay = true, the Delay activity doesn't trigger PersistableIdle + /// + [Fact] + public static void TestBlockingDelayShouldNotPersist() + { + var testSequence = new Sequence() + { + Activities = + { + new Delay() + { + Duration = TimeSpan.FromMilliseconds(100) + }, + } + }; + bool workflowIdleAndPersistable = false; + var completed = new ManualResetEvent(false); + WorkflowApplication workflowApplication = new WorkflowApplication(testSequence); + workflowApplication.InstanceStore = new MemoryInstanceStore(); + workflowApplication.Extensions.Add(new StatementsBehaviorExtension { BlockingDelay = true }); + workflowApplication.PersistableIdle = (_) => { workflowIdleAndPersistable = true; return PersistableIdleAction.None; }; + workflowApplication.Completed = (_) => completed.Set(); + workflowApplication.Run(); + completed.WaitOne(); + workflowIdleAndPersistable.ShouldBeFalse(); //there is no persistable idle + } + + /// + /// Tests that without using StatementsBehaviorExtension extension, the Delay activity triggers PersistableIdle + /// + [Fact] + public static void TestNonBlockingDelayShouldPersist() + { + var testSequence = new Sequence() + { + Activities = + { + new Delay() + { + Duration = TimeSpan.FromMilliseconds(100) + }, + } + }; + bool workflowIdleAndPersistable = false; + var completed = new ManualResetEvent(false); + WorkflowApplication workflowApplication = new WorkflowApplication(testSequence); + workflowApplication.InstanceStore = new MemoryInstanceStore(); + workflowApplication.PersistableIdle = (_) => { workflowIdleAndPersistable = true; return PersistableIdleAction.None; }; + workflowApplication.Completed = (_) => completed.Set(); + workflowApplication.Run(); + completed.WaitOne(); + workflowIdleAndPersistable.ShouldBeTrue(); + } + + /// + /// Tests that having StatementsBehaviorExtension extension with BlockingDelay=true, the Delay activity blocks the + /// PersistableIdle event from any other activity in the workflow, including parallel activities. + /// + [Fact] + public static void TestParallelBlockingActivityShouldTriggerPersistAfterDelayFinishes() + { + var delayDuration = TimeSpan.FromMilliseconds(100); + + var testSequence = new Sequence() + { + Activities = + { + new Parallel() + { + Branches = + { + new Delay() { Duration = delayDuration }, + new BlockingActivity("B") + } + }, + } + }; + bool workflowIdleAndPersistable = false; + var completed = new ManualResetEvent(false); + var persistableIdleTriggered = new ManualResetEvent(false); + + var sw = new System.Diagnostics.Stopwatch(); + + WorkflowApplication workflowApplication = new WorkflowApplication(testSequence); + workflowApplication.InstanceStore = new MemoryInstanceStore(); + workflowApplication.Extensions.Add(new StatementsBehaviorExtension { BlockingDelay = true }); + + + workflowApplication.PersistableIdle = (_) => { + //check if more than 100ms passed + sw.ElapsedMilliseconds.ShouldBeGreaterThan(delayDuration.Milliseconds); + workflowIdleAndPersistable = true; + persistableIdleTriggered.Set(); + return PersistableIdleAction.None; + }; + workflowApplication.Completed = (_) => completed.Set(); + sw.Start(); + workflowApplication.Run(); + persistableIdleTriggered.WaitOne(); // Wait for PersistableIdle to trigger + workflowApplication.ResumeBookmark("B", null); //Resume the bookmark from the blocking activity + completed.WaitOne(); + workflowIdleAndPersistable.ShouldBeTrue(); + } + } +} diff --git a/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs b/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs index 6488215c..a0547725 100644 --- a/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs +++ b/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs @@ -377,7 +377,7 @@ public static void TestResumeWithDelay() { new TestDelay() { - Duration = TimeSpan.FromMilliseconds(100) + Duration = TimeSpan.FromMilliseconds(200) }, } }; @@ -393,7 +393,7 @@ public static void TestResumeWithDelay() [Fact] public static void TestNoPersistSerialization() { - TestSequence testSequence = new() { Activities = { new TestNoPersist() }}; + TestSequence testSequence = new() { Activities = { new TestNoPersist() } }; WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(testSequence, null, jsonStore, PersistableIdleAction.Unload); workflowRuntime.ExecuteWorkflow(); diff --git a/src/UiPath.Workflow.Runtime/Statements/Delay.cs b/src/UiPath.Workflow.Runtime/Statements/Delay.cs index e7d93a65..09fccf49 100644 --- a/src/UiPath.Workflow.Runtime/Statements/Delay.cs +++ b/src/UiPath.Workflow.Runtime/Statements/Delay.cs @@ -4,6 +4,7 @@ using System.Activities.Runtime; using System.Collections.ObjectModel; using System.Windows.Markup; +using UiPath.Workflow.Runtime.Statements; namespace System.Activities.Statements; @@ -12,6 +13,8 @@ public sealed class Delay : NativeActivity { private static readonly Func getDefaultTimerExtension = new Func(GetDefaultTimerExtension); private readonly Variable _timerBookmark; + private readonly Variable _noPersistHandle = new Variable(); + public Delay() : base() @@ -23,6 +26,7 @@ public Delay() [DefaultValue(null)] public InArgument Duration { get; set; } + protected override bool CanInduceIdle => true; protected override void CacheMetadata(NativeActivityMetadata metadata) @@ -31,6 +35,7 @@ protected override void CacheMetadata(NativeActivityMetadata metadata) metadata.Bind(Duration, durationArgument); metadata.SetArgumentsCollection(new Collection { durationArgument }); metadata.AddImplementationVariable(_timerBookmark); + metadata.AddImplementationVariable(_noPersistHandle); metadata.AddDefaultExtensionProvider(getDefaultTimerExtension); } @@ -50,6 +55,10 @@ protected override void Execute(NativeActivityContext context) } TimerExtension timerExtension = GetTimerExtension(context); + + if (HasBlockingDelay(context)) + _noPersistHandle.Get(context).Enter(context); + Bookmark bookmark = context.CreateBookmark(); timerExtension.RegisterTimer(duration, bookmark); _timerBookmark.Set(context, bookmark); @@ -82,4 +91,9 @@ private TimerExtension GetTimerExtension(ActivityContext context) Fx.Assert(timerExtension != null, "TimerExtension must exist."); return timerExtension; } + + private bool HasBlockingDelay(NativeActivityContext context) + { + return context.GetExtension()?.BlockingDelay == true; + } } diff --git a/src/UiPath.Workflow.Runtime/Statements/StatementsBehaviorExtension.cs b/src/UiPath.Workflow.Runtime/Statements/StatementsBehaviorExtension.cs new file mode 100644 index 00000000..b1a14b8c --- /dev/null +++ b/src/UiPath.Workflow.Runtime/Statements/StatementsBehaviorExtension.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UiPath.Workflow.Runtime.Statements +{ + /// + /// An extension that configures/changes the behavior of some statements like Delay. + /// + public class StatementsBehaviorExtension + { + /// + /// When true, the delay activity will block the persistance idle event until the delay is over, + /// preventing the workflow from being persisted. + /// + public bool BlockingDelay { get; set; } = false; + } +}