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;
+ }
+}