From f0317714e5bf804bca15c649a1aea89a679fa09e Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Thu, 2 May 2024 18:36:38 +0300 Subject: [PATCH 01/17] [BTS support] Add SetCurrentParallelBranch --- .../TestCases.Runtime/ParallelBranchTests.cs | 101 ++++++++++ .../Persistence/AbstractInstanceStore.cs | 177 ++++++++++++++++++ .../DataContractWorkflowSerializer.cs | 162 ++++++++++++++++ .../Persistence/FileInstanceStore.cs | 31 +++ .../Persistence/JsonWorkflowSerializer.cs | 36 ++++ .../Persistence/Resources.cs | 9 + .../WorkflowApplicationTestExtensions.cs | 150 ++++++++------- .../WorkflowApplicationTestExtensions.csproj | 4 +- .../WorkflowApplicationTestSamples.cs | 4 +- .../ParallelTrackingExtensions.cs | 51 ++++- 10 files changed, 641 insertions(+), 84 deletions(-) create mode 100644 src/Test/TestCases.Runtime/ParallelBranchTests.cs create mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs create mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs create mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs create mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs create mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs new file mode 100644 index 00000000..b314ad35 --- /dev/null +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -0,0 +1,101 @@ +using Shouldly; +using System; +using System.Activities; +using System.Activities.ParallelTracking; +using System.Linq; +using WorkflowApplicationTestExtensions; +using Xunit; + +namespace TestCases.Runtime; + +public class ParallelBranchTests +{ + public class TestCodeActivity(Action onExecute) : CodeActivity + { + protected override void Execute(CodeActivityContext context) + { + onExecute(context); + } + } + private static void Run(params Action[] onExecute) + => new WorkflowApplication(new SuspendingWrapper(onExecute.Select(c => new TestCodeActivity(c)).ToArray())) + .RunUntilCompletion(); + + [Fact] + public void Push() + { + var level1 = new ParallelBranch().Push(); + var level2 = level1.Push(); + var level3 = level2.Push(); + level2.BranchesStackString.ShouldStartWith(level1.BranchesStackString); + level3.BranchesStackString.ShouldStartWith(level2.BranchesStackString); + var l3Splits = level3.BranchesStackString.Split('.'); + l3Splits.Length.ShouldBe(3); + l3Splits.First().ShouldBe(level1.BranchesStackString); + } + + [Fact] + public void GetAndSet() + { + + static void SaveParent(ActivityInstance instance, ParallelBranch parentLevel) + { + new ExecutionProperties(null, instance, instance.PropertyManager) + .Add("localParallelBranch", parentLevel, skipValidations: true, onlyVisibleToPublicChildren: false); + } + static ParallelBranch GetParent(ActivityInstance instance) + { + return (ParallelBranch)new ExecutionProperties(null, instance, instance.PropertyManager) + .Find("localParallelBranch"); + } + + ParallelBranch parentLevel = default; + Run(SetParent, + ParallelBranchPersistence, + GetCurrentParallelBranch_InheritsFromParent, + PushAndSetParallelBranch, + ParallelBranchDoesNotLeakToSiblings + ); + + void SetParent(CodeActivityContext context) + { + var parent = context.CurrentInstance.Parent; + parent.MarkNewParallelBranch(); + parentLevel = parent.GetCurrentParallelBranch(); + SaveParent(parent, parentLevel); + } + void ParallelBranchPersistence(CodeActivityContext context) + { + var persistedParent = GetParent(context.CurrentInstance); + var branchId = context.GetCurrentParallelBranchId(); + persistedParent.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + branchId.ShouldBe(persistedParent.BranchesStackString); + persistedParent.InstanceId.ShouldBe(parentLevel.InstanceId); + persistedParent.InstanceId.ShouldBe(context.CurrentInstance.Parent.Id); + } + void GetCurrentParallelBranch_InheritsFromParent(CodeActivityContext context) + { + var branchId = context.GetCurrentParallelBranchId(); + var currentBranch = context.CurrentInstance.GetCurrentParallelBranch(); + currentBranch.BranchesStackString.ShouldBe(branchId); + currentBranch.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + } + void PushAndSetParallelBranch(CodeActivityContext context) + { + var pushLevelOnSchedule = parentLevel.Push(); + var scheduledInstance = context.CurrentInstance; + scheduledInstance.SetCurrentParallelBranch(pushLevelOnSchedule); + var getPushedLevel = scheduledInstance.GetCurrentParallelBranch(); + getPushedLevel.BranchesStackString.ShouldBe(pushLevelOnSchedule.BranchesStackString); + scheduledInstance.GetCurrentParallelBranchId().ShouldBe(pushLevelOnSchedule.BranchesStackString); + } + void ParallelBranchDoesNotLeakToSiblings(CodeActivityContext context) + { + var readLevel = context.CurrentInstance.GetCurrentParallelBranch(); + var branchId = context.GetCurrentParallelBranchId(); + readLevel.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + branchId.ShouldBe(parentLevel.BranchesStackString); + } + } +} + diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs new file mode 100644 index 00000000..52586dc5 --- /dev/null +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs @@ -0,0 +1,177 @@ +using System; +using System.Activities.DurableInstancing; +using System.Diagnostics; +using System.IO; +using System.Activities.Runtime.DurableInstancing; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Nito.AsyncEx.Interop; + +namespace WorkflowApplicationTestExtensions.Persistence; + + +using InstanceDictionary = Dictionary; +using XInstanceDictionary = IDictionary; +public interface IWorkflowSerializer +{ + /// + /// Load workflow instance values from the sourceStream + /// + /// The source stream for the persisted data + /// Workflow instance state to be used + public XInstanceDictionary LoadWorkflowInstance(Stream sourceStream); + /// + /// Persist the workflow instance values into the destination stream + /// + /// The workflow instance state + /// The destination stream for the persisted data + public void SaveWorkflowInstance(XInstanceDictionary workflowInstanceState, Stream destinationStream); +} + +static class WorkflowSerializerHelpers +{ + public static InstanceDictionary ToSave(this XInstanceDictionary source) => source + .Where(property => !property.Value.Options.HasFlag(InstanceValueOptions.WriteOnly) && !property.Value.IsDeletedValue) + .ToDictionary(property => property.Key.ToString(), property => property.Value); + public static XInstanceDictionary ToNameDictionary(object source) => ((InstanceDictionary)source) + .ToDictionary(sourceItem => (XName)sourceItem.Key, sourceItem => sourceItem.Value); +} + +public abstract class AbstractInstanceStore(IWorkflowSerializer instanceSerializer) : InstanceStore +{ + private Guid _storageInstanceId = Guid.NewGuid(); + private Guid _lockId = Guid.NewGuid(); + private readonly IWorkflowSerializer _instanceSerializer = instanceSerializer; + + private class StreamWrapperWithDiposeEvent(Stream stream, Action onDispose) : Stream + { + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + onDispose?.Invoke(); + } + + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position { get => stream.Position; set => stream.Position = value; } + + public override void Flush() + { + stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return stream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + stream.Write(buffer, offset, count); + } + } + + protected Stream OnStreamDispose(Stream stream, Action onDispose) => new StreamWrapperWithDiposeEvent(stream, onDispose); + + protected abstract Task GetReadStream(Guid instanceId); + protected abstract Task GetWriteStream(Guid instanceId); + + protected sealed override IAsyncResult BeginTryCommand(InstancePersistenceContext context, InstancePersistenceCommand command, TimeSpan timeout, AsyncCallback callback, object state) + { + return ApmAsyncFactory.ToBegin(TryCommandAndYield(), callback, state); + async Task TryCommandAndYield() + { + try + { + var result = await TryCommandAsync(context, command); + // When TryCommandAsync completes synchronously, prevent chaining the WF state machine steps + // which can lead to large stack, e.g. BeginRun->ActivityComplete->Persist->Complete + await Task.Yield(); + return result; + } + catch (Exception ex) + { + throw new Exception(ex.Message, ex); + } + } + } + + protected sealed override bool EndTryCommand(IAsyncResult result) + { + var task = (Task)result; + return task.Result; + } + + private async Task TryCommandAsync(InstancePersistenceContext context, InstancePersistenceCommand command) + { + if (command is CreateWorkflowOwnerCommand) + { + CreateWorkflowOwner(context); + } + else if (command is CreateWorkflowOwnerWithIdentityCommand) + { + CreateWorkflowOwnerWithIdentity(context); + } + else if (command is SaveWorkflowCommand) + { + await SaveWorkflow(context, (SaveWorkflowCommand)command); + } + else if (command is LoadWorkflowCommand) + { + await LoadWorkflow(context); + } + else if (command is DeleteWorkflowOwnerCommand) + { + //Nothing to 'delete', we don't implement locking + } + else + { + // This trace is for dev team, not actionable by end user. + Trace.TraceInformation($"Persistence command {command.Name} is not supported."); + return false; + } + + Trace.TraceInformation($"Persistence command {command.Name} issued for job with Id {context.InstanceView.InstanceId}."); + return true; + } + + private async Task LoadWorkflow(InstancePersistenceContext context) + { + using var stream = await GetReadStream(context.InstanceView.InstanceId); + context.LoadedInstance(InstanceState.Initialized, _instanceSerializer.LoadWorkflowInstance(stream), null, null, null); + } + + private async Task SaveWorkflow(InstancePersistenceContext context, SaveWorkflowCommand command) + { + using var stream = await GetWriteStream(context.InstanceView.InstanceId); + _instanceSerializer.SaveWorkflowInstance(command.InstanceData, stream); + } + + private void CreateWorkflowOwner(InstancePersistenceContext context) + { + context.BindInstanceOwner(_storageInstanceId, _lockId); + } + + private void CreateWorkflowOwnerWithIdentity(InstancePersistenceContext context) + { + context.BindInstanceOwner(_storageInstanceId, _lockId); + } +} diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs new file mode 100644 index 00000000..402235fd --- /dev/null +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs @@ -0,0 +1,162 @@ +using System.IO; +using System.Activities.Runtime.DurableInstancing; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Runtime.Serialization; +using System.Collections; +using System.Xml; +using System; +using System.Activities; +using System.Reflection; + +namespace WorkflowApplicationTestExtensions.Persistence; + + +using XInstanceDictionary = IDictionary; +using InstanceDictionary = Dictionary; + +public interface IFaultHandler +{ + void OnFault(NativeActivityFaultContext faultContext, Exception propagatedException, ActivityInstance propagatedFrom); + void OnComplete(NativeActivityContext context, ActivityInstance completedInstance); +} + +public sealed class DataContractWorkflowSerializer : IWorkflowSerializer +{ + public XInstanceDictionary LoadWorkflowInstance(Stream sourceStream) => + WorkflowSerializerHelpers.ToNameDictionary(GetDataContractSerializer().ReadObject(sourceStream)); + + public void SaveWorkflowInstance(XInstanceDictionary workflowInstanceState, Stream destinationStream) => + GetDataContractSerializer().WriteObject(destinationStream, workflowInstanceState.ToSave()); + + private static DataContractSerializer GetDataContractSerializer() + { + DataContractSerializerSettings settings = new DataContractSerializerSettings + { + PreserveObjectReferences = true, + DataContractResolver = new WorkflowDataContractResolver() + }; + var dataContractSerializer = new DataContractSerializer(typeof(InstanceDictionary), settings); + dataContractSerializer.SetSerializationSurrogateProvider(new SerializationSurrogateProvider()); + return dataContractSerializer; + } + + /// + /// WF knows how to serialize Delegates and callbacks, but *only* if they are an Activity method + /// See https://referencesource.microsoft.com/#System.Activities/System/Activities/Runtime/CallbackWrapper.cs,273 + /// Because of how global exception handling and debug tracking is implemented, we are breaking that assumption + /// We force a serialization surrogate that knows how to handle our delegates + /// We're replacing the serialization of ActivityCompletionCallbackWrapper and FaultCallbackWrapper + /// see https://github.com/Microsoft/referencesource/blob/master/System.Activities/System/Activities/Runtime/CallbackWrapper.cs + /// + [DataContract] + internal sealed class SurrogateWrapper + { + [DataMember] + public bool IsFaultCallback { get; set; } + + [DataMember] + public IFaultHandler Handler { get; set; } + + [DataMember] + public ActivityInstance ActivityInstance { get; set; } + } + + internal sealed class SerializationSurrogateProvider : ISerializationSurrogateProvider + { + private const string FaultWrapperTypeName = "System.Activities.Runtime.FaultCallbackWrapper"; + private const string CompletionWrapperTypeName = "System.Activities.Runtime.ActivityCompletionCallbackWrapper"; + private const string NewtonsoftUnserializableNamespace = "Newtonsoft.Json.Linq"; + + private static bool IsWrapperType(Type type) => type.FullName == FaultWrapperTypeName || type.FullName == CompletionWrapperTypeName; + public Type GetSurrogateType(Type type) => IsWrapperType(type) ? typeof(SurrogateWrapper) : null; + + public object GetObjectToSerialize(object obj, Type targetType) + { + var typeToSerialize = obj.GetType(); + System.Diagnostics.Trace.TraceInformation($"TypeToSerialize = {typeToSerialize.FullName}"); + //to be removed after .NET8 upgrade ROBO-2615 + if (typeToSerialize.FullName.Contains(NewtonsoftUnserializableNamespace)) + { + throw new InvalidDataContractException(string.Format(Resources.NewtonsoftTypesSerializationError, NewtonsoftUnserializableNamespace, typeToSerialize.FullName)); + } + + if (!IsWrapperType(typeToSerialize)) + { + return obj; + } + + Delegate callback = GetPrivateField(obj, "_callback"); + if (callback?.Target is not IFaultHandler handler) + { + return obj; + } + return new SurrogateWrapper + { + IsFaultCallback = callback is FaultCallback, + Handler = handler, + ActivityInstance = GetPrivateField(obj, "_activityInstance"), + }; + } + + private static T GetPrivateField(object obj, string fieldName) + { + var field = GetFieldInfo(obj.GetType(), fieldName); + var value = field?.GetValue(obj); + return value is T t ? t : default; + } + + private static FieldInfo GetFieldInfo(Type type, string fieldName) + { + return type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); + } + + public object GetDeserializedObject(object obj, Type targetType) + { + if (obj is not SurrogateWrapper surrogate) + { + return obj; + } + var originalType = typeof(Activity).Assembly.GetType(surrogate.IsFaultCallback + ? FaultWrapperTypeName + : CompletionWrapperTypeName); + + return Activator.CreateInstance(originalType, surrogate.IsFaultCallback + ? (FaultCallback)surrogate.Handler.OnFault + : (CompletionCallback)surrogate.Handler.OnComplete, + surrogate.ActivityInstance); + } + } + + private sealed class WorkflowDataContractResolver : DataContractResolver + { + private readonly Dictionary _cachedTypes = new(); + + public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) => + _cachedTypes.TryGetValue(typeName, out var cachedType) ? cachedType : FindType(typeName); + + public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) + { + if (typeof(IEnumerable) == declaredType) //ROBO-2904 + { + typeName = null; + typeNamespace = null; + return true; + } + + typeName = new XmlDictionaryString(XmlDictionary.Empty, type.AssemblyQualifiedName, 0); + typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, type.Namespace, 0); + + return true; + } + + private Type FindType(string qualifiedTypeName) + { + var type = Type.GetType(qualifiedTypeName, throwOnError: true); + _cachedTypes[qualifiedTypeName] = type; + + return type; + } + } + +} diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs new file mode 100644 index 00000000..d8a325b3 --- /dev/null +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace WorkflowApplicationTestExtensions.Persistence; + +public class FileInstanceStore : AbstractInstanceStore +{ + private readonly string _storeDirectoryPath; + + public FileInstanceStore(string storeDirectoryPath) : this(new JsonWorkflowSerializer(), storeDirectoryPath) { } + + public FileInstanceStore(IWorkflowSerializer workflowSerializer, string storeDirectoryPath) : base(workflowSerializer) + { + _storeDirectoryPath = storeDirectoryPath; + Directory.CreateDirectory(storeDirectoryPath); + } + + protected override Task GetReadStream(Guid instanceId) + { + return Task.FromResult(File.OpenRead(_storeDirectoryPath + "\\" + instanceId + "-InstanceData")); + } + + protected override Task GetWriteStream(Guid instanceId) + { + var filePath = _storeDirectoryPath + "\\" + instanceId + "-InstanceData"; + File.Delete(filePath); + return Task.FromResult(File.OpenWrite(filePath)); + } + +} diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs new file mode 100644 index 00000000..723f3756 --- /dev/null +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Activities.Runtime.DurableInstancing; +using System.Collections.Generic; +using System.Xml.Linq; +using Newtonsoft.Json; + +namespace WorkflowApplicationTestExtensions.Persistence; + + +using InstanceDictionary = Dictionary; +using XInstanceDictionary = IDictionary; + +public class JsonWorkflowSerializer : IWorkflowSerializer +{ + XInstanceDictionary IWorkflowSerializer.LoadWorkflowInstance(Stream sourceStream) + { + JsonTextReader reader = new(new StreamReader(sourceStream)); + var workflowState = Serializer().Deserialize(reader, typeof(InstanceDictionary)); + return WorkflowSerializerHelpers.ToNameDictionary(workflowState); + } + void IWorkflowSerializer.SaveWorkflowInstance(XInstanceDictionary workflowInstanceState, Stream destinationStream) + { + JsonTextWriter writer = new(new StreamWriter(destinationStream)); + Serializer().Serialize(writer, workflowInstanceState.ToSave()); + writer.Flush(); + } + private static JsonSerializer Serializer() => new() + { + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.Auto, + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + ObjectCreationHandling = ObjectCreationHandling.Replace + }; + +} diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs new file mode 100644 index 00000000..de6b1af1 --- /dev/null +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs @@ -0,0 +1,9 @@ +using System; + +namespace WorkflowApplicationTestExtensions.Persistence +{ + internal class Resources + { + internal static readonly string NewtonsoftTypesSerializationError = "NewtonsoftTypesSerializationError"; + } +} \ No newline at end of file diff --git a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs index 7f63c8b0..825a096e 100644 --- a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs +++ b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs @@ -1,96 +1,102 @@ -using JsonFileInstanceStore; +using WorkflowApplicationTestExtensions.Persistence; using System; using System.Activities; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using StringToObject = System.Collections.Generic.IDictionary; -namespace WorkflowApplicationTestExtensions +namespace WorkflowApplicationTestExtensions; + +public static class WorkflowApplicationTestExtensions { - public static class WorkflowApplicationTestExtensions - { - public const string AutoResumedBookmarkNamePrefix = "AutoResumedBookmark_"; + public const string AutoResumedBookmarkNamePrefix = "AutoResumedBookmark_"; - public record WorkflowApplicationResult(StringToObject Outputs, int PersistenceCount); + public record WorkflowApplicationResult(StringToObject Outputs, int PersistenceCount); - /// - /// Simple API to wait for the workflow to complete or propagate to the caller any error. - /// Also, when PersistableIdle, will automatically Unload, Load, resume some bookmarks - /// (those named "AutoResumedBookmark_...") and continue execution. - /// - public static WorkflowApplicationResult RunUntilCompletion(this WorkflowApplication application) + /// + /// Simple API to wait for the workflow to complete or propagate to the caller any error. + /// Also, when PersistableIdle, will automatically Unload, Load, resume some bookmarks + /// (those named "AutoResumedBookmark_...") and continue execution. + /// + public static WorkflowApplicationResult RunUntilCompletion(this WorkflowApplication application, Action beforeResume = null) + { + var applicationId = application.Id; + var persistenceCount = 0; + var output = new TaskCompletionSource(); + application.Completed += (WorkflowApplicationCompletedEventArgs args) => { - var persistenceCount = 0; - var output = new TaskCompletionSource(); - application.Completed += (WorkflowApplicationCompletedEventArgs args) => + if (args.TerminationException is { } ex) { - if (args.TerminationException is { } ex) - { - output.TrySetException(ex); - } - if (args.CompletionState == ActivityInstanceState.Canceled) - { - throw new OperationCanceledException("Workflow canceled."); - } - output.TrySetResult(new(args.Outputs, persistenceCount)); - }; - - application.Aborted += args => output.TrySetException(args.Reason); - - application.InstanceStore = new FileInstanceStore(Environment.CurrentDirectory); - application.PersistableIdle += (WorkflowApplicationIdleEventArgs args) => + output.TrySetException(ex); + } + if (args.CompletionState == ActivityInstanceState.Canceled) { - Debug.WriteLine("PersistableIdle"); - var bookmarks = args.Bookmarks; - Task.Delay(100).ContinueWith(_ => - { - try - { - if (++persistenceCount > 100) - { - throw new Exception("Persisting too many times, aborting test."); - } - application = CloneWorkflowApplication(application); - application.Load(args.InstanceId); - foreach (var bookmark in bookmarks) - { - application.ResumeBookmark(new Bookmark(bookmark.BookmarkName), null); - } - } - catch (Exception ex) - { - output.TrySetException(ex); - } - }); - return PersistableIdleAction.Unload; - }; + throw new OperationCanceledException("Workflow canceled."); + } + output.TrySetResult(new(args.Outputs, persistenceCount)); + application = null; + }; - application.BeginRun(null, null); + application.Aborted += args => + { + output.TrySetException(args.Reason); + }; + + application.InstanceStore ??= new FileInstanceStore(Environment.CurrentDirectory); + application.Unloaded += uargs => + { + Debug.WriteLine("Unloaded"); + if (application == null) + return; + application.Load(applicationId); + foreach (var bookmark in application.GetBookmarks().Where(b => b.BookmarkName.StartsWith(AutoResumedBookmarkNamePrefix))) + { + application.ResumeBookmark(new Bookmark(bookmark.BookmarkName), null); + } + }; + application.PersistableIdle += (WorkflowApplicationIdleEventArgs args) => + { + Debug.WriteLine("PersistableIdle"); try { - output.Task.Wait(TimeSpan.FromSeconds(15)); + if (++persistenceCount > 1000) + { + throw new Exception("Persisting too many times, aborting test."); + } + application = CloneWorkflowApplication(application); + beforeResume?.Invoke(application); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (Exception ex) { + output.TrySetException(ex); } - return output.Task.GetAwaiter().GetResult(); - } + return PersistableIdleAction.Unload; + }; + + application.Run(); - private static WorkflowApplication CloneWorkflowApplication(WorkflowApplication application) + try + { + output.Task.Wait(TimeSpan.FromSeconds(15)); + } + catch (Exception ex) when (ex is not OperationCanceledException) { - var clone = new WorkflowApplication(application.WorkflowDefinition, application.DefinitionIdentity) - { - Aborted = application.Aborted, - Completed = application.Completed, - PersistableIdle = application.PersistableIdle, - InstanceStore = application.InstanceStore, - }; - foreach (var extension in application.Extensions.GetAllSingletonExtensions()) - { - clone.Extensions.Add(extension); - } - return clone; } + return output.Task.GetAwaiter().GetResult(); + } + + private static WorkflowApplication CloneWorkflowApplication(WorkflowApplication application) + { + var clone = new WorkflowApplication(application.WorkflowDefinition, application.DefinitionIdentity) + { + Aborted = application.Aborted, + Completed = application.Completed, + PersistableIdle = application.PersistableIdle, + Unloaded = application.Unloaded, + InstanceStore = application.InstanceStore, + }; + return clone; } } diff --git a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.csproj b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.csproj index 787ec943..90ed8443 100644 --- a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.csproj +++ b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.csproj @@ -1,5 +1,5 @@  - + - \ No newline at end of file + diff --git a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestSamples.cs b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestSamples.cs index bc5549a7..9c123144 100644 --- a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestSamples.cs +++ b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestSamples.cs @@ -24,7 +24,7 @@ public void RunUntilCompletion_Outputs() public void RunUntilCompletion_Faulted() { var app = new WorkflowApplication(new Throw { Exception = new InArgument(_ => new ArgumentException()) }); - Should.Throw(app.RunUntilCompletion); + Should.Throw(() => app.RunUntilCompletion()); } [Fact(Skip="Flaky")] @@ -32,7 +32,7 @@ public void RunUntilCompletion_Aborted() { var app = new WorkflowApplication(new Delay { Duration = TimeSpan.MaxValue }); Task.Delay(10).ContinueWith(_ => app.Abort()); - Should.Throw(app.RunUntilCompletion); + Should.Throw(() => app.RunUntilCompletion()); } [Fact] diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index bd61376d..7be7aee0 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -1,4 +1,21 @@ -namespace System.Activities.ParallelTracking; +using System.Diagnostics; +namespace System.Activities.ParallelTracking; + +[DataContract] +public record struct ParallelBranch +{ + [DataMember] + internal string InstanceId { get; init; } + + [DataMember] + internal string BranchesStackString { get; init; } + + public readonly ParallelBranch Push() => new() + { + BranchesStackString = $"{BranchesStackString}.{Guid.NewGuid():N}".Trim('.'), + InstanceId = InstanceId + }; +} /// /// This feature introduces a context.GetCurrentParallelId() queryable from an activity, @@ -13,19 +30,37 @@ public static class ParallelTrackingExtensions public static ActivityInstance MarkNewParallelBranch(this ActivityInstance instance) { - var properties = new ExecutionProperties(null, instance, instance.PropertyManager); - var parentId = properties.Find(BranchIdPropertyName) as string; - properties.Add(BranchIdPropertyName, $"{parentId}.{Guid.NewGuid():N}".Trim('.'), true, false); - + var parentId = instance.GetCurrentParallelBranchId(); + var newBranch = new ParallelBranch() { BranchesStackString = parentId }.Push(); + instance.SetCurrentParallelBranchId(newBranch.BranchesStackString); return instance; } public static string GetCurrentParallelBranchId(this ActivityInstance instance) => instance.PropertyManager?.GetPropertyAtCurrentScope(BranchIdPropertyName) as string; - public static ActivityInstance MarkNewParallelBranch(this ActivityContext context) => - context.CurrentInstance?.MarkNewParallelBranch(); - public static string GetCurrentParallelBranchId(this ActivityContext context) => context.CurrentInstance?.GetCurrentParallelBranchId(); + + public static ParallelBranch GetCurrentParallelBranch(this ActivityInstance instance) => + new() + { + BranchesStackString = instance.GetCurrentParallelBranchId(), + InstanceId = instance.Id + }; + + public static void SetCurrentParallelBranch(this ActivityInstance currentOrChildInstance, ParallelBranch parallelBranch) + { + if (currentOrChildInstance.Id != parallelBranch.InstanceId && currentOrChildInstance?.Parent?.Id != parallelBranch.InstanceId) + throw new ArgumentException($"{nameof(ParallelBranch)} must be obtained from this activity instance or it's parent.", nameof(currentOrChildInstance)); + + currentOrChildInstance.SetCurrentParallelBranchId(parallelBranch.BranchesStackString); + } + + private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) => + GetExecutionProperties(instance) + .Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); + + private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => + new(null, instance, instance.PropertyManager); } From 5a46d8dc96e347aa63cb66ea87c8823a1e07bf41 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Thu, 2 May 2024 21:42:44 +0300 Subject: [PATCH 02/17] add stack validation --- .../TestCases.Runtime/ParallelBranchTests.cs | 155 +++++++++++------- .../ParallelTrackingExtensions.cs | 49 ++++-- 2 files changed, 129 insertions(+), 75 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index b314ad35..0aced170 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -17,16 +17,29 @@ protected override void Execute(CodeActivityContext context) onExecute(context); } } - private static void Run(params Action[] onExecute) - => new WorkflowApplication(new SuspendingWrapper(onExecute.Select(c => new TestCodeActivity(c)).ToArray())) - .RunUntilCompletion(); + + ParallelBranch parentLevel = default; + private void Run(params Action[] onExecute) + { + var execs = new Action[] { new(SetParent) } + .Concat(onExecute); + new WorkflowApplication(new SuspendingWrapper(execs.Select(c => new TestCodeActivity(c)))) + .RunUntilCompletion(); + + void SetParent(CodeActivityContext context) + { + var parent = context.CurrentInstance.Parent; + parent.MarkNewParallelBranch(); + parentLevel = parent.GetCurrentParallelBranch(); + } + } [Fact] public void Push() { var level1 = new ParallelBranch().Push(); var level2 = level1.Push(); - var level3 = level2.Push(); + var level3 = level2.Push(); level2.BranchesStackString.ShouldStartWith(level1.BranchesStackString); level3.BranchesStackString.ShouldStartWith(level2.BranchesStackString); var l3Splits = level3.BranchesStackString.Split('.'); @@ -35,67 +48,95 @@ public void Push() } [Fact] - public void GetAndSet() + public void ParallelBranchPersistence() => Run( + context => { + PersistParallelBranch(); - static void SaveParent(ActivityInstance instance, ParallelBranch parentLevel) + void PersistParallelBranch() { - new ExecutionProperties(null, instance, instance.PropertyManager) + new ExecutionProperties(null, context.CurrentInstance.Parent, context.CurrentInstance.Parent.PropertyManager) .Add("localParallelBranch", parentLevel, skipValidations: true, onlyVisibleToPublicChildren: false); } - static ParallelBranch GetParent(ActivityInstance instance) + }, + context => + { + var persistedParent = GetPersistedParallelBranch(); + var branchId = context.GetCurrentParallelBranchId(); + persistedParent.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + branchId.ShouldBe(persistedParent.BranchesStackString); + + ParallelBranch GetPersistedParallelBranch() { - return (ParallelBranch)new ExecutionProperties(null, instance, instance.PropertyManager) + return (ParallelBranch)new ExecutionProperties(null, context.CurrentInstance.Parent, context.CurrentInstance.Parent.PropertyManager) .Find("localParallelBranch"); } + }); - ParallelBranch parentLevel = default; - Run(SetParent, - ParallelBranchPersistence, - GetCurrentParallelBranch_InheritsFromParent, - PushAndSetParallelBranch, - ParallelBranchDoesNotLeakToSiblings - ); + [Fact] + public void GetCurrentParallelBranch_InheritsFromParent() => Run( + context => + { + var branchId = context.GetCurrentParallelBranchId(); + var currentBranch = context.CurrentInstance.GetCurrentParallelBranch(); + currentBranch.BranchesStackString.ShouldBe(branchId); + currentBranch.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + }); - void SetParent(CodeActivityContext context) - { - var parent = context.CurrentInstance.Parent; - parent.MarkNewParallelBranch(); - parentLevel = parent.GetCurrentParallelBranch(); - SaveParent(parent, parentLevel); - } - void ParallelBranchPersistence(CodeActivityContext context) - { - var persistedParent = GetParent(context.CurrentInstance); - var branchId = context.GetCurrentParallelBranchId(); - persistedParent.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); - branchId.ShouldBe(persistedParent.BranchesStackString); - persistedParent.InstanceId.ShouldBe(parentLevel.InstanceId); - persistedParent.InstanceId.ShouldBe(context.CurrentInstance.Parent.Id); - } - void GetCurrentParallelBranch_InheritsFromParent(CodeActivityContext context) - { - var branchId = context.GetCurrentParallelBranchId(); - var currentBranch = context.CurrentInstance.GetCurrentParallelBranch(); - currentBranch.BranchesStackString.ShouldBe(branchId); - currentBranch.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); - } - void PushAndSetParallelBranch(CodeActivityContext context) - { - var pushLevelOnSchedule = parentLevel.Push(); - var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranch(pushLevelOnSchedule); - var getPushedLevel = scheduledInstance.GetCurrentParallelBranch(); - getPushedLevel.BranchesStackString.ShouldBe(pushLevelOnSchedule.BranchesStackString); - scheduledInstance.GetCurrentParallelBranchId().ShouldBe(pushLevelOnSchedule.BranchesStackString); - } - void ParallelBranchDoesNotLeakToSiblings(CodeActivityContext context) - { - var readLevel = context.CurrentInstance.GetCurrentParallelBranch(); - var branchId = context.GetCurrentParallelBranchId(); - readLevel.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); - branchId.ShouldBe(parentLevel.BranchesStackString); - } - } -} + [Fact] + public void PushAndSetParallelBranch() => Run( + context => + { + var pushLevelOnSchedule = parentLevel.Push(); + var scheduledInstance = context.CurrentInstance; + scheduledInstance.SetCurrentParallelBranch(pushLevelOnSchedule); + var getPushedLevel = scheduledInstance.GetCurrentParallelBranch(); + getPushedLevel.BranchesStackString.ShouldBe(pushLevelOnSchedule.BranchesStackString); + scheduledInstance.GetCurrentParallelBranchId().ShouldBe(pushLevelOnSchedule.BranchesStackString); + }); + + [Fact] + public void UnparentedPushFails() => Run( + context => + { + var instance = context.CurrentInstance; + instance.SetCurrentParallelBranch(parentLevel.Push()); + Should.Throw(() => instance.SetCurrentParallelBranch(parentLevel.Push().Push())); + }); + [Fact] + public void DoublePush() => Run( + context => + { + var instance = context.CurrentInstance; + instance.SetCurrentParallelBranch(parentLevel.Push().Push()); + }); + + [Fact] + public void DoublePop() => Run( + context => + { + var instance = context.CurrentInstance; + instance.SetCurrentParallelBranch(parentLevel.Push().Push()); + instance.SetCurrentParallelBranch(parentLevel); + }); + + [Fact] + public void UnparentedPopFails() => Run( + context => + { + var scheduledInstance = context.CurrentInstance; + scheduledInstance.SetCurrentParallelBranch(parentLevel.Push().Push()); + Should.Throw(() => scheduledInstance.SetCurrentParallelBranch(parentLevel.Push())); + }); + + [Fact] + public void ParallelBranchDoesNotLeakToSiblings() => Run( + context => + { + var readLevel = context.CurrentInstance.GetCurrentParallelBranch(); + var branchId = context.GetCurrentParallelBranchId(); + readLevel.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + branchId.ShouldBe(parentLevel.BranchesStackString); + }); +} diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index 7be7aee0..0845413a 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -1,20 +1,30 @@ -using System.Diagnostics; -namespace System.Activities.ParallelTracking; +namespace System.Activities.ParallelTracking; [DataContract] public record struct ParallelBranch { - [DataMember] - internal string InstanceId { get; init; } + private const char StackDelimiter = '.'; [DataMember] internal string BranchesStackString { get; init; } + [DataMember] + internal string InstanceId { get; init; } + + public readonly ParallelBranch Push() => + new() + { + BranchesStackString = $"{BranchesStackString}.{Guid.NewGuid():N}".Trim('.'), + InstanceId = InstanceId + }; - public readonly ParallelBranch Push() => new() + internal bool IsAncestorOf(ParallelBranch descendantBranch) { - BranchesStackString = $"{BranchesStackString}.{Guid.NewGuid():N}".Trim('.'), - InstanceId = InstanceId - }; + var descendantStack = descendantBranch.BranchesStackString ?? string.Empty; + var lastIndex = descendantStack.LastIndexOf(StackDelimiter); + var thisStack = BranchesStackString ?? string.Empty; + return thisStack.StartsWith(descendantStack, StringComparison.Ordinal) + && descendantBranch.InstanceId == InstanceId; + } } /// @@ -43,23 +53,26 @@ public static ActivityInstance MarkNewParallelBranch(this ActivityInstance insta context.CurrentInstance?.GetCurrentParallelBranchId(); public static ParallelBranch GetCurrentParallelBranch(this ActivityInstance instance) => - new() - { - BranchesStackString = instance.GetCurrentParallelBranchId(), - InstanceId = instance.Id - }; + new() { BranchesStackString = instance.GetCurrentParallelBranchId() }; public static void SetCurrentParallelBranch(this ActivityInstance currentOrChildInstance, ParallelBranch parallelBranch) { - if (currentOrChildInstance.Id != parallelBranch.InstanceId && currentOrChildInstance?.Parent?.Id != parallelBranch.InstanceId) - throw new ArgumentException($"{nameof(ParallelBranch)} must be obtained from this activity instance or it's parent.", nameof(currentOrChildInstance)); + var currentBranch = currentOrChildInstance.GetCurrentParallelBranch(); + if (!parallelBranch.IsAncestorOf(currentBranch) + && !currentBranch.IsAncestorOf(parallelBranch)) + throw new ArgumentException($"{nameof(parallelBranch)} must be a pop or a push.", nameof(currentOrChildInstance)); currentOrChildInstance.SetCurrentParallelBranchId(parallelBranch.BranchesStackString); } - private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) => - GetExecutionProperties(instance) - .Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); + private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) + { + var props = GetExecutionProperties(instance); + if (props.Find(BranchIdPropertyName) is not null) + props.Remove(BranchIdPropertyName, skipValidations: true); + + props.Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); + } private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => new(null, instance, instance.PropertyManager); From 2e7d3cc8ea6ae724e500de4f1864f96bd5b7dd3d Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Fri, 3 May 2024 12:32:01 +0300 Subject: [PATCH 03/17] add json and datacontract variations with memoryInstanceStore --- .../TestCases.Runtime/ParallelBranchTests.cs | 13 ++++- .../ParallelTrackingExtensionsTests.cs | 16 +++++- .../Persistence/AbstractInstanceStore.cs | 54 +++++++++++++------ .../Persistence/FileInstanceStore.cs | 8 ++- .../Persistence/MemoryInstanceStore.cs | 27 ++++++++++ .../WorkflowApplicationTestExtensions.cs | 2 +- 6 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index 0aced170..15e93eef 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -4,10 +4,17 @@ using System.Activities.ParallelTracking; using System.Linq; using WorkflowApplicationTestExtensions; +using WorkflowApplicationTestExtensions.Persistence; using Xunit; namespace TestCases.Runtime; + +public class ParallelBranchTestsJson : ParallelBranchTests +{ + protected override IWorkflowSerializer Serializer => new JsonWorkflowSerializer(); +} + public class ParallelBranchTests { public class TestCodeActivity(Action onExecute) : CodeActivity @@ -18,12 +25,16 @@ protected override void Execute(CodeActivityContext context) } } + protected virtual IWorkflowSerializer Serializer => new DataContractWorkflowSerializer(); ParallelBranch parentLevel = default; private void Run(params Action[] onExecute) { var execs = new Action[] { new(SetParent) } .Concat(onExecute); - new WorkflowApplication(new SuspendingWrapper(execs.Select(c => new TestCodeActivity(c)))) + new WorkflowApplication(new SuspendingWrapper(execs.Select(c => new TestCodeActivity(c)))) + { + InstanceStore = new MemoryInstanceStore(Serializer) + } .RunUntilCompletion(); void SetParent(CodeActivityContext context) diff --git a/src/Test/TestCases.Runtime/ParallelTrackingExtensionsTests.cs b/src/Test/TestCases.Runtime/ParallelTrackingExtensionsTests.cs index 8cc4bab9..9de59402 100644 --- a/src/Test/TestCases.Runtime/ParallelTrackingExtensionsTests.cs +++ b/src/Test/TestCases.Runtime/ParallelTrackingExtensionsTests.cs @@ -7,11 +7,19 @@ using System.Linq; using WorkflowApplicationTestExtensions; using Xunit; +using WorkflowApplicationTestExtensions.Persistence; namespace TestCases.Runtime { + public class ParallelTrackingExtensionsTestsJson : ParallelTrackingExtensionsTests + { + protected override IWorkflowSerializer Serializer => new JsonWorkflowSerializer(); + } + public class ParallelTrackingExtensionsTests { + protected virtual IWorkflowSerializer Serializer => new DataContractWorkflowSerializer(); + [Fact] public void ParallelActivity() { @@ -89,8 +97,12 @@ public void PickActivity() trigger1Id.ShouldNotBe(trigger2Id); } - private static void Run(Activity activity) => - new WorkflowApplication(activity).RunUntilCompletion(); + private void Run(Activity activity) => + new WorkflowApplication(activity) + { + InstanceStore = new MemoryInstanceStore(Serializer) + } + .RunUntilCompletion(); private static void ValidateId(string id, int expectedNesting, string shouldStartWith = null) { diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs index 52586dc5..dd34b541 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs @@ -41,55 +41,67 @@ public static XInstanceDictionary ToNameDictionary(object source) => ((InstanceD public abstract class AbstractInstanceStore(IWorkflowSerializer instanceSerializer) : InstanceStore { - private Guid _storageInstanceId = Guid.NewGuid(); - private Guid _lockId = Guid.NewGuid(); + private readonly Guid _storageInstanceId = Guid.NewGuid(); + private readonly Guid _lockId = Guid.NewGuid(); private readonly IWorkflowSerializer _instanceSerializer = instanceSerializer; - private class StreamWrapperWithDiposeEvent(Stream stream, Action onDispose) : Stream + private class StreamWrapperWithDisposeEvent : Stream { + private readonly Stream _stream; + public Action OnDispose { get; init; } + private readonly Guid _instanceId; + + public StreamWrapperWithDisposeEvent(Stream stream, Guid instanceId) + { + _stream = stream; + _instanceId = instanceId; + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); - onDispose?.Invoke(); + _stream.Dispose(); + OnDispose?.Invoke(_instanceId, _stream); } - public override bool CanRead => stream.CanRead; + public override bool CanRead => _stream.CanRead; - public override bool CanSeek => stream.CanSeek; + public override bool CanSeek => _stream.CanSeek; - public override bool CanWrite => stream.CanWrite; + public override bool CanWrite => _stream.CanWrite; - public override long Length => stream.Length; + public override long Length => _stream.Length; - public override long Position { get => stream.Position; set => stream.Position = value; } + public override long Position { get => _stream.Position; set => _stream.Position = value; } public override void Flush() { - stream.Flush(); + _stream.Flush(); } public override int Read(byte[] buffer, int offset, int count) { - return stream.Read(buffer, offset, count); + return _stream.Read(buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) { - return stream.Seek(offset, origin); + return _stream.Seek(offset, origin); } public override void SetLength(long value) { - stream.SetLength(value); + _stream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) { - stream.Write(buffer, offset, count); + _stream.Write(buffer, offset, count); } } - protected Stream OnStreamDispose(Stream stream, Action onDispose) => new StreamWrapperWithDiposeEvent(stream, onDispose); + protected virtual void OnReadStreamDisposed(Guid instanceId, Stream stream) { } + protected virtual void OnWriteStreamDisposed(Guid instanceId, Stream stream) { } protected abstract Task GetReadStream(Guid instanceId); protected abstract Task GetWriteStream(Guid instanceId); @@ -155,13 +167,21 @@ private async Task TryCommandAsync(InstancePersistenceContext context, Ins private async Task LoadWorkflow(InstancePersistenceContext context) { - using var stream = await GetReadStream(context.InstanceView.InstanceId); + var originalStream = await GetReadStream(context.InstanceView.InstanceId); + using var stream = new StreamWrapperWithDisposeEvent(originalStream, context.InstanceView.InstanceId) + { + OnDispose = OnReadStreamDisposed + }; context.LoadedInstance(InstanceState.Initialized, _instanceSerializer.LoadWorkflowInstance(stream), null, null, null); } private async Task SaveWorkflow(InstancePersistenceContext context, SaveWorkflowCommand command) { - using var stream = await GetWriteStream(context.InstanceView.InstanceId); + var originalStream = await GetWriteStream(context.InstanceView.InstanceId); + using var stream = new StreamWrapperWithDisposeEvent(originalStream, context.InstanceView.InstanceId) + { + OnDispose = OnWriteStreamDisposed + }; _instanceSerializer.SaveWorkflowInstance(command.InstanceData, stream); } diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs index d8a325b3..c7301e3a 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs @@ -18,14 +18,18 @@ public FileInstanceStore(IWorkflowSerializer workflowSerializer, string storeDir protected override Task GetReadStream(Guid instanceId) { - return Task.FromResult(File.OpenRead(_storeDirectoryPath + "\\" + instanceId + "-InstanceData")); + return Task.FromResult(File.OpenRead(GetFilePath(instanceId))); } protected override Task GetWriteStream(Guid instanceId) { - var filePath = _storeDirectoryPath + "\\" + instanceId + "-InstanceData"; + string filePath = GetFilePath(instanceId); File.Delete(filePath); return Task.FromResult(File.OpenWrite(filePath)); } + private string GetFilePath(Guid instanceId) + { + return _storeDirectoryPath + "\\" + instanceId + "-InstanceData"; + } } diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs new file mode 100644 index 00000000..06bc27bc --- /dev/null +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace WorkflowApplicationTestExtensions.Persistence; + +public class MemoryInstanceStore(IWorkflowSerializer workflowSerializer) : AbstractInstanceStore(workflowSerializer) +{ + private readonly ConcurrentDictionary _cache = new(); + + public MemoryInstanceStore() : this(new JsonWorkflowSerializer()) { } + + protected override void OnReadStreamDisposed(Guid instanceId, Stream stream) + => _cache.Remove(instanceId, out _); + + protected override Task GetReadStream(Guid instanceId) + { + _cache[instanceId].TryGetBuffer(out var buffer); + return Task.FromResult(new MemoryStream(buffer.Array, buffer.Offset, buffer.Count)); + } + + protected override Task GetWriteStream(Guid instanceId) + => Task.FromResult(_cache[instanceId] = new MemoryStream()); +} + diff --git a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs index 825a096e..10781c5b 100644 --- a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs +++ b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs @@ -43,7 +43,7 @@ public static WorkflowApplicationResult RunUntilCompletion(this WorkflowApplicat output.TrySetException(args.Reason); }; - application.InstanceStore ??= new FileInstanceStore(Environment.CurrentDirectory); + application.InstanceStore ??= new MemoryInstanceStore(new DataContractWorkflowSerializer()); application.Unloaded += uargs => { Debug.WriteLine("Unloaded"); From fd62c318ac9a3f399bb9486c0e8e290cdca15e2b Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Wed, 8 May 2024 14:40:13 +0300 Subject: [PATCH 04/17] proper use of Execution props on set --- .../TestCases.Runtime/ParallelBranchTests.cs | 20 +++++++++++++++++ .../ParallelTrackingExtensions.cs | 22 +++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index 15e93eef..85581af9 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -58,6 +58,26 @@ public void Push() l3Splits.First().ShouldBe(level1.BranchesStackString); } + [Fact] + public void SetToNullWhenNull() => Run( + context => + { + context.CurrentInstance.SetCurrentParallelBranch(default); + context.CurrentInstance.SetCurrentParallelBranch(default); + context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); + }); + + [Fact] + public void SetToNullWhenNotNull() => Run( + context => + { + context.CurrentInstance.SetCurrentParallelBranch(default); + context.CurrentInstance.SetCurrentParallelBranch(default(ParallelBranch).Push()); + context.CurrentInstance.SetCurrentParallelBranch(default); + context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); + }); + + [Fact] public void ParallelBranchPersistence() => Run( context => diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index 0845413a..b49e019d 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -13,14 +13,13 @@ public record struct ParallelBranch public readonly ParallelBranch Push() => new() { - BranchesStackString = $"{BranchesStackString}.{Guid.NewGuid():N}".Trim('.'), + BranchesStackString = $"{BranchesStackString}.{Guid.NewGuid():N}".Trim(StackDelimiter), InstanceId = InstanceId }; - internal bool IsAncestorOf(ParallelBranch descendantBranch) + internal readonly bool IsAncestorOf(ParallelBranch descendantBranch) { var descendantStack = descendantBranch.BranchesStackString ?? string.Empty; - var lastIndex = descendantStack.LastIndexOf(StackDelimiter); var thisStack = BranchesStackString ?? string.Empty; return thisStack.StartsWith(descendantStack, StringComparison.Ordinal) && descendantBranch.InstanceId == InstanceId; @@ -67,13 +66,18 @@ public static void SetCurrentParallelBranch(this ActivityInstance currentOrChild private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) { - var props = GetExecutionProperties(instance); - if (props.Find(BranchIdPropertyName) is not null) - props.Remove(BranchIdPropertyName, skipValidations: true); + RemoveIfExists(); + GetExecutionProperties(instance).Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); - props.Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); + void RemoveIfExists() + { + if (instance.PropertyManager?.IsOwner(instance) is true + && instance.PropertyManager.Properties.ContainsKey(BranchIdPropertyName)) + instance.PropertyManager.Remove(BranchIdPropertyName); + } } - private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => - new(null, instance, instance.PropertyManager); + + private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => + new(null, instance, instance.PropertyManager?.IsOwner(instance) is true ? instance.PropertyManager : null); } From f452bd96ac81561e0e97fa9a16aab6bdf9089681 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Wed, 8 May 2024 15:37:12 +0300 Subject: [PATCH 05/17] prepare giveup on PrallelBranch --- .../ParallelTrackingExtensions.cs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index b49e019d..f4a9948f 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -1,10 +1,8 @@ namespace System.Activities.ParallelTracking; [DataContract] -public record struct ParallelBranch +public readonly record struct ParallelBranch { - private const char StackDelimiter = '.'; - [DataMember] internal string BranchesStackString { get; init; } [DataMember] @@ -13,17 +11,9 @@ public record struct ParallelBranch public readonly ParallelBranch Push() => new() { - BranchesStackString = $"{BranchesStackString}.{Guid.NewGuid():N}".Trim(StackDelimiter), + BranchesStackString = ParallelTrackingExtensions.PushNewBranch(BranchesStackString), InstanceId = InstanceId }; - - internal readonly bool IsAncestorOf(ParallelBranch descendantBranch) - { - var descendantStack = descendantBranch.BranchesStackString ?? string.Empty; - var thisStack = BranchesStackString ?? string.Empty; - return thisStack.StartsWith(descendantStack, StringComparison.Ordinal) - && descendantBranch.InstanceId == InstanceId; - } } /// @@ -36,12 +26,13 @@ public record struct ParallelBranch public static class ParallelTrackingExtensions { public const string BranchIdPropertyName = "__ParallelBranchId"; + private const char StackDelimiter = '.'; + public static ActivityInstance MarkNewParallelBranch(this ActivityInstance instance) { var parentId = instance.GetCurrentParallelBranchId(); - var newBranch = new ParallelBranch() { BranchesStackString = parentId }.Push(); - instance.SetCurrentParallelBranchId(newBranch.BranchesStackString); + instance.SetCurrentParallelBranchId(PushNewBranch(parentId)); return instance; } @@ -56,9 +47,7 @@ public static ActivityInstance MarkNewParallelBranch(this ActivityInstance insta public static void SetCurrentParallelBranch(this ActivityInstance currentOrChildInstance, ParallelBranch parallelBranch) { - var currentBranch = currentOrChildInstance.GetCurrentParallelBranch(); - if (!parallelBranch.IsAncestorOf(currentBranch) - && !currentBranch.IsAncestorOf(parallelBranch)) + if (parallelBranch.InstanceId is not null && parallelBranch.InstanceId != currentOrChildInstance.Id) throw new ArgumentException($"{nameof(parallelBranch)} must be a pop or a push.", nameof(currentOrChildInstance)); currentOrChildInstance.SetCurrentParallelBranchId(parallelBranch.BranchesStackString); @@ -66,6 +55,11 @@ public static void SetCurrentParallelBranch(this ActivityInstance currentOrChild private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) { + var currentBranchId = instance.GetCurrentParallelBranchId(); + + if (!IsAncestorOf(thisStack: branchId, descendantStack: currentBranchId) + && !IsAncestorOf(thisStack: currentBranchId, descendantStack: branchId)) + throw new ArgumentException($"{nameof(branchId)} must be a pop or a push.", nameof(instance)); RemoveIfExists(); GetExecutionProperties(instance).Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); @@ -77,7 +71,11 @@ void RemoveIfExists() } } - private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => new(null, instance, instance.PropertyManager?.IsOwner(instance) is true ? instance.PropertyManager : null); + private static bool IsAncestorOf(string thisStack, string descendantStack) => + (thisStack ?? string.Empty) + .StartsWith(descendantStack ?? string.Empty, StringComparison.Ordinal); + internal static string PushNewBranch(string thisStack) => + $"{thisStack}.{Guid.NewGuid():N}".Trim(StackDelimiter); } From e82ea0c45ced16c186a1226b2a31b5eae2d8f69c Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Thu, 9 May 2024 10:17:17 +0300 Subject: [PATCH 06/17] fix parallelBranch ownership --- src/Test/TestCases.Runtime/ParallelBranchTests.cs | 13 ++++++++----- .../ParallelTrackingExtensions.cs | 12 +++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index 85581af9..2a17f0bb 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -27,6 +27,7 @@ protected override void Execute(CodeActivityContext context) protected virtual IWorkflowSerializer Serializer => new DataContractWorkflowSerializer(); ParallelBranch parentLevel = default; + ParallelBranch noBranch = default; private void Run(params Action[] onExecute) { var execs = new Action[] { new(SetParent) } @@ -39,7 +40,9 @@ private void Run(params Action[] onExecute) void SetParent(CodeActivityContext context) { + var parent = context.CurrentInstance.Parent; + noBranch = parent.GetCurrentParallelBranch(); parent.MarkNewParallelBranch(); parentLevel = parent.GetCurrentParallelBranch(); } @@ -62,8 +65,8 @@ public void Push() public void SetToNullWhenNull() => Run( context => { - context.CurrentInstance.SetCurrentParallelBranch(default); - context.CurrentInstance.SetCurrentParallelBranch(default); + context.CurrentInstance.SetCurrentParallelBranch(noBranch); + context.CurrentInstance.SetCurrentParallelBranch(noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); }); @@ -71,9 +74,9 @@ public void Push() public void SetToNullWhenNotNull() => Run( context => { - context.CurrentInstance.SetCurrentParallelBranch(default); - context.CurrentInstance.SetCurrentParallelBranch(default(ParallelBranch).Push()); - context.CurrentInstance.SetCurrentParallelBranch(default); + context.CurrentInstance.SetCurrentParallelBranch(noBranch); + context.CurrentInstance.SetCurrentParallelBranch(noBranch.Push()); + context.CurrentInstance.SetCurrentParallelBranch(noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); }); diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index f4a9948f..42e7b697 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -43,11 +43,12 @@ public static ActivityInstance MarkNewParallelBranch(this ActivityInstance insta context.CurrentInstance?.GetCurrentParallelBranchId(); public static ParallelBranch GetCurrentParallelBranch(this ActivityInstance instance) => - new() { BranchesStackString = instance.GetCurrentParallelBranchId() }; + new() { BranchesStackString = instance.GetCurrentParallelBranchId(), InstanceId = instance.Id }; public static void SetCurrentParallelBranch(this ActivityInstance currentOrChildInstance, ParallelBranch parallelBranch) { - if (parallelBranch.InstanceId is not null && parallelBranch.InstanceId != currentOrChildInstance.Id) + if (parallelBranch.InstanceId != currentOrChildInstance.Id + && parallelBranch.InstanceId != currentOrChildInstance.Parent?.Id) throw new ArgumentException($"{nameof(parallelBranch)} must be a pop or a push.", nameof(currentOrChildInstance)); currentOrChildInstance.SetCurrentParallelBranchId(parallelBranch.BranchesStackString); @@ -65,17 +66,18 @@ private static void SetCurrentParallelBranchId(this ActivityInstance instance, s void RemoveIfExists() { - if (instance.PropertyManager?.IsOwner(instance) is true - && instance.PropertyManager.Properties.ContainsKey(BranchIdPropertyName)) + if (instance.PropertyManager?.IsOwner(instance) is true) instance.PropertyManager.Remove(BranchIdPropertyName); } } private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => - new(null, instance, instance.PropertyManager?.IsOwner(instance) is true ? instance.PropertyManager : null); + new(null, instance, instance.PropertyManager); + private static bool IsAncestorOf(string thisStack, string descendantStack) => (thisStack ?? string.Empty) .StartsWith(descendantStack ?? string.Empty, StringComparison.Ordinal); + internal static string PushNewBranch(string thisStack) => $"{thisStack}.{Guid.NewGuid():N}".Trim(StackDelimiter); } From 8091cb51c163c8f6b39fd4048388a9c8a40e4f48 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Thu, 9 May 2024 16:13:44 +0300 Subject: [PATCH 07/17] set parallel branch to null removes from current instance and inherits from parent --- .../TestCases.Runtime/ParallelBranchTests.cs | 34 +++++++++++-------- .../ParallelTrackingExtensions.cs | 20 ++++++----- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index 2a17f0bb..d62f66e3 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -28,19 +28,23 @@ protected override void Execute(CodeActivityContext context) protected virtual IWorkflowSerializer Serializer => new DataContractWorkflowSerializer(); ParallelBranch parentLevel = default; ParallelBranch noBranch = default; + private void Run(params Action[] onExecute) { - var execs = new Action[] { new(SetParent) } - .Concat(onExecute); - new WorkflowApplication(new SuspendingWrapper(execs.Select(c => new TestCodeActivity(c)))) + new WorkflowApplication(new SuspendingWrapper(onExecute.Select(c => new TestCodeActivity(c)))) { InstanceStore = new MemoryInstanceStore(Serializer) } .RunUntilCompletion(); + } + + private void RunWithParent(params Action[] onExecute) + { + Run([new(SetParent), .. onExecute]); void SetParent(CodeActivityContext context) { - + var parent = context.CurrentInstance.Parent; noBranch = parent.GetCurrentParallelBranch(); parent.MarkNewParallelBranch(); @@ -62,9 +66,10 @@ public void Push() } [Fact] - public void SetToNullWhenNull() => Run( + public void RestoreToNullWhenNull() => Run( context => { + var noBranch = context.CurrentInstance.GetCurrentParallelBranch(); context.CurrentInstance.SetCurrentParallelBranch(noBranch); context.CurrentInstance.SetCurrentParallelBranch(noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); @@ -73,7 +78,8 @@ public void Push() [Fact] public void SetToNullWhenNotNull() => Run( context => - { + { + var noBranch = context.CurrentInstance.GetCurrentParallelBranch(); context.CurrentInstance.SetCurrentParallelBranch(noBranch); context.CurrentInstance.SetCurrentParallelBranch(noBranch.Push()); context.CurrentInstance.SetCurrentParallelBranch(noBranch); @@ -82,7 +88,7 @@ public void Push() [Fact] - public void ParallelBranchPersistence() => Run( + public void ParallelBranchPersistence() => RunWithParent( context => { PersistParallelBranch(); @@ -108,7 +114,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void GetCurrentParallelBranch_InheritsFromParent() => Run( + public void GetCurrentParallelBranch_InheritsFromParent() => RunWithParent( context => { var branchId = context.GetCurrentParallelBranchId(); @@ -118,7 +124,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void PushAndSetParallelBranch() => Run( + public void PushAndSetParallelBranch() => RunWithParent( context => { var pushLevelOnSchedule = parentLevel.Push(); @@ -130,7 +136,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void UnparentedPushFails() => Run( + public void UnparentedPushFails() => RunWithParent( context => { var instance = context.CurrentInstance; @@ -139,7 +145,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void DoublePush() => Run( + public void DoublePush() => RunWithParent( context => { var instance = context.CurrentInstance; @@ -147,7 +153,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void DoublePop() => Run( + public void DoublePop() => RunWithParent( context => { var instance = context.CurrentInstance; @@ -156,7 +162,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void UnparentedPopFails() => Run( + public void UnparentedPopFails() => RunWithParent( context => { var scheduledInstance = context.CurrentInstance; @@ -165,7 +171,7 @@ ParallelBranch GetPersistedParallelBranch() }); [Fact] - public void ParallelBranchDoesNotLeakToSiblings() => Run( + public void ParallelBranchDoesNotLeakToSiblings() => RunWithParent( context => { var readLevel = context.CurrentInstance.GetCurrentParallelBranch(); diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index 42e7b697..1ba88560 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -37,7 +37,7 @@ public static ActivityInstance MarkNewParallelBranch(this ActivityInstance insta } public static string GetCurrentParallelBranchId(this ActivityInstance instance) => - instance.PropertyManager?.GetPropertyAtCurrentScope(BranchIdPropertyName) as string; + GetExecutionProperties(instance).Find(BranchIdPropertyName) as string; public static string GetCurrentParallelBranchId(this ActivityContext context) => context.CurrentInstance?.GetCurrentParallelBranchId(); @@ -54,6 +54,11 @@ public static void SetCurrentParallelBranch(this ActivityInstance currentOrChild currentOrChildInstance.SetCurrentParallelBranchId(parallelBranch.BranchesStackString); } + /// + /// Sets the parallelBranchId for the current activity instance + /// + /// null or empty removes the branch setting from current instance + /// when not a pop or a push private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) { var currentBranchId = instance.GetCurrentParallelBranchId(); @@ -61,14 +66,13 @@ private static void SetCurrentParallelBranchId(this ActivityInstance instance, s if (!IsAncestorOf(thisStack: branchId, descendantStack: currentBranchId) && !IsAncestorOf(thisStack: currentBranchId, descendantStack: branchId)) throw new ArgumentException($"{nameof(branchId)} must be a pop or a push.", nameof(instance)); - RemoveIfExists(); - GetExecutionProperties(instance).Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); - void RemoveIfExists() - { - if (instance.PropertyManager?.IsOwner(instance) is true) - instance.PropertyManager.Remove(BranchIdPropertyName); - } + var props = GetExecutionProperties(instance); + props.Remove(BranchIdPropertyName, skipValidations: true); + if (string.IsNullOrEmpty(branchId)) + return; + + props.Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); } private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => From 382b49751828c48b9573d72b9d68621bc7ed1b1d Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Thu, 9 May 2024 16:58:27 +0300 Subject: [PATCH 08/17] remove ParallelBranch --- .../TestCases.Runtime/ParallelBranchTests.cs | 109 +++++++----------- .../ParallelTrackingExtensions.cs | 39 +------ 2 files changed, 46 insertions(+), 102 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index d62f66e3..f8477bc3 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -26,17 +26,15 @@ protected override void Execute(CodeActivityContext context) } protected virtual IWorkflowSerializer Serializer => new DataContractWorkflowSerializer(); - ParallelBranch parentLevel = default; - ParallelBranch noBranch = default; + string _parentLevel = default; + readonly string _noBranch = default; private void Run(params Action[] onExecute) { new WorkflowApplication(new SuspendingWrapper(onExecute.Select(c => new TestCodeActivity(c)))) { InstanceStore = new MemoryInstanceStore(Serializer) - } - .RunUntilCompletion(); - + }.RunUntilCompletion(); } private void RunWithParent(params Action[] onExecute) @@ -44,34 +42,32 @@ private void RunWithParent(params Action[] onExecute) Run([new(SetParent), .. onExecute]); void SetParent(CodeActivityContext context) { - var parent = context.CurrentInstance.Parent; - noBranch = parent.GetCurrentParallelBranch(); + parent.GetCurrentParallelBranchId().ShouldBe(_noBranch); parent.MarkNewParallelBranch(); - parentLevel = parent.GetCurrentParallelBranch(); + _parentLevel = parent.GetCurrentParallelBranchId(); } } [Fact] public void Push() { - var level1 = new ParallelBranch().Push(); - var level2 = level1.Push(); - var level3 = level2.Push(); - level2.BranchesStackString.ShouldStartWith(level1.BranchesStackString); - level3.BranchesStackString.ShouldStartWith(level2.BranchesStackString); - var l3Splits = level3.BranchesStackString.Split('.'); + var level1 = default(string).PushNewBranch(); + var level2 = level1.PushNewBranch(); + var level3 = level2.PushNewBranch(); + level2.ShouldStartWith(level1); + level3.ShouldStartWith(level2); + var l3Splits = level3.Split('.'); l3Splits.Length.ShouldBe(3); - l3Splits.First().ShouldBe(level1.BranchesStackString); + l3Splits.First().ShouldBe(level1); } [Fact] - public void RestoreToNullWhenNull() => Run( + public void SetToNullWhenNull() => Run( context => { - var noBranch = context.CurrentInstance.GetCurrentParallelBranch(); - context.CurrentInstance.SetCurrentParallelBranch(noBranch); - context.CurrentInstance.SetCurrentParallelBranch(noBranch); + context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); + context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); }); @@ -79,60 +75,33 @@ public void Push() public void SetToNullWhenNotNull() => Run( context => { - var noBranch = context.CurrentInstance.GetCurrentParallelBranch(); - context.CurrentInstance.SetCurrentParallelBranch(noBranch); - context.CurrentInstance.SetCurrentParallelBranch(noBranch.Push()); - context.CurrentInstance.SetCurrentParallelBranch(noBranch); + context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); + context.CurrentInstance.SetCurrentParallelBranchId(_noBranch.PushNewBranch()); + context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); }); - [Fact] - public void ParallelBranchPersistence() => RunWithParent( - context => - { - PersistParallelBranch(); - - void PersistParallelBranch() - { - new ExecutionProperties(null, context.CurrentInstance.Parent, context.CurrentInstance.Parent.PropertyManager) - .Add("localParallelBranch", parentLevel, skipValidations: true, onlyVisibleToPublicChildren: false); - } - }, - context => - { - var persistedParent = GetPersistedParallelBranch(); - var branchId = context.GetCurrentParallelBranchId(); - persistedParent.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); - branchId.ShouldBe(persistedParent.BranchesStackString); - - ParallelBranch GetPersistedParallelBranch() - { - return (ParallelBranch)new ExecutionProperties(null, context.CurrentInstance.Parent, context.CurrentInstance.Parent.PropertyManager) - .Find("localParallelBranch"); - } - }); - [Fact] public void GetCurrentParallelBranch_InheritsFromParent() => RunWithParent( context => { var branchId = context.GetCurrentParallelBranchId(); - var currentBranch = context.CurrentInstance.GetCurrentParallelBranch(); - currentBranch.BranchesStackString.ShouldBe(branchId); - currentBranch.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); + var currentBranch = context.CurrentInstance.GetCurrentParallelBranchId(); + currentBranch.ShouldBe(branchId); + currentBranch.ShouldBe(_parentLevel); }); [Fact] public void PushAndSetParallelBranch() => RunWithParent( context => { - var pushLevelOnSchedule = parentLevel.Push(); + var pushLevelOnSchedule = _parentLevel.PushNewBranch(); var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranch(pushLevelOnSchedule); - var getPushedLevel = scheduledInstance.GetCurrentParallelBranch(); - getPushedLevel.BranchesStackString.ShouldBe(pushLevelOnSchedule.BranchesStackString); - scheduledInstance.GetCurrentParallelBranchId().ShouldBe(pushLevelOnSchedule.BranchesStackString); + scheduledInstance.SetCurrentParallelBranchId(pushLevelOnSchedule); + var getPushedLevel = scheduledInstance.GetCurrentParallelBranchId(); + getPushedLevel.ShouldBe(pushLevelOnSchedule); + scheduledInstance.GetCurrentParallelBranchId().ShouldBe(pushLevelOnSchedule); }); [Fact] @@ -140,8 +109,8 @@ ParallelBranch GetPersistedParallelBranch() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranch(parentLevel.Push()); - Should.Throw(() => instance.SetCurrentParallelBranch(parentLevel.Push().Push())); + instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch()); + Should.Throw(() => instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch())); }); [Fact] @@ -149,7 +118,7 @@ ParallelBranch GetPersistedParallelBranch() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranch(parentLevel.Push().Push()); + instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch()); }); [Fact] @@ -157,8 +126,8 @@ ParallelBranch GetPersistedParallelBranch() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranch(parentLevel.Push().Push()); - instance.SetCurrentParallelBranch(parentLevel); + instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch()); + instance.SetCurrentParallelBranchId(_parentLevel); }); [Fact] @@ -166,17 +135,23 @@ ParallelBranch GetPersistedParallelBranch() context => { var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranch(parentLevel.Push().Push()); - Should.Throw(() => scheduledInstance.SetCurrentParallelBranch(parentLevel.Push())); + scheduledInstance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch()); + Should.Throw(() => scheduledInstance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch())); }); [Fact] public void ParallelBranchDoesNotLeakToSiblings() => RunWithParent( context => { - var readLevel = context.CurrentInstance.GetCurrentParallelBranch(); + context.CurrentInstance.SetCurrentParallelBranchId(context.CurrentInstance.GetCurrentParallelBranchId().PushNewBranch()); + context.CurrentInstance.GetCurrentParallelBranchId().ShouldNotBe(_parentLevel); + context.CurrentInstance.GetCurrentParallelBranchId().ShouldStartWith(_parentLevel); + }, + context => + { + var readLevel = context.CurrentInstance.GetCurrentParallelBranchId(); var branchId = context.GetCurrentParallelBranchId(); - readLevel.BranchesStackString.ShouldBe(parentLevel.BranchesStackString); - branchId.ShouldBe(parentLevel.BranchesStackString); + readLevel.ShouldBe(_parentLevel); + branchId.ShouldBe(_parentLevel); }); } diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index 1ba88560..d001d659 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -1,21 +1,5 @@ namespace System.Activities.ParallelTracking; -[DataContract] -public readonly record struct ParallelBranch -{ - [DataMember] - internal string BranchesStackString { get; init; } - [DataMember] - internal string InstanceId { get; init; } - - public readonly ParallelBranch Push() => - new() - { - BranchesStackString = ParallelTrackingExtensions.PushNewBranch(BranchesStackString), - InstanceId = InstanceId - }; -} - /// /// This feature introduces a context.GetCurrentParallelId() queryable from an activity, /// which should identify a "parallelism" branch on which the activity executes. @@ -25,9 +9,7 @@ /// public static class ParallelTrackingExtensions { - public const string BranchIdPropertyName = "__ParallelBranchId"; - private const char StackDelimiter = '.'; - + private const string BranchIdPropertyName = "__ParallelBranchId"; public static ActivityInstance MarkNewParallelBranch(this ActivityInstance instance) { @@ -42,24 +24,12 @@ public static ActivityInstance MarkNewParallelBranch(this ActivityInstance insta public static string GetCurrentParallelBranchId(this ActivityContext context) => context.CurrentInstance?.GetCurrentParallelBranchId(); - public static ParallelBranch GetCurrentParallelBranch(this ActivityInstance instance) => - new() { BranchesStackString = instance.GetCurrentParallelBranchId(), InstanceId = instance.Id }; - - public static void SetCurrentParallelBranch(this ActivityInstance currentOrChildInstance, ParallelBranch parallelBranch) - { - if (parallelBranch.InstanceId != currentOrChildInstance.Id - && parallelBranch.InstanceId != currentOrChildInstance.Parent?.Id) - throw new ArgumentException($"{nameof(parallelBranch)} must be a pop or a push.", nameof(currentOrChildInstance)); - - currentOrChildInstance.SetCurrentParallelBranchId(parallelBranch.BranchesStackString); - } - /// /// Sets the parallelBranchId for the current activity instance /// /// null or empty removes the branch setting from current instance /// when not a pop or a push - private static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) + public static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) { var currentBranchId = instance.GetCurrentParallelBranchId(); @@ -74,6 +44,8 @@ private static void SetCurrentParallelBranchId(this ActivityInstance instance, s props.Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); } + public static string PushNewBranch(this string thisStack) => + $"{thisStack}.{Guid.NewGuid():N}".Trim('.'); private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => new(null, instance, instance.PropertyManager); @@ -81,7 +53,4 @@ private static void SetCurrentParallelBranchId(this ActivityInstance instance, s private static bool IsAncestorOf(string thisStack, string descendantStack) => (thisStack ?? string.Empty) .StartsWith(descendantStack ?? string.Empty, StringComparison.Ordinal); - - internal static string PushNewBranch(string thisStack) => - $"{thisStack}.{Guid.NewGuid():N}".Trim(StackDelimiter); } From 99d6bfb48e0b7690f297b32a0be4a7b86f474557 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Fri, 10 May 2024 14:12:48 +0300 Subject: [PATCH 09/17] review comments --- .../TestCases.Runtime/ParallelBranchTests.cs | 26 +++++++++---------- .../Persistence/JsonWorkflowSerializer.cs | 5 ++-- .../ParallelTrackingExtensions.cs | 4 +-- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index f8477bc3..17e9aefd 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -39,7 +39,7 @@ private void Run(params Action[] onExecute) private void RunWithParent(params Action[] onExecute) { - Run([new(SetParent), .. onExecute]); + Run([SetParent, .. onExecute]); void SetParent(CodeActivityContext context) { var parent = context.CurrentInstance.Parent; @@ -52,9 +52,9 @@ void SetParent(CodeActivityContext context) [Fact] public void Push() { - var level1 = default(string).PushNewBranch(); - var level2 = level1.PushNewBranch(); - var level3 = level2.PushNewBranch(); + var level1 = ParallelTrackingExtensions.PushNewBranch(null); + var level2 = ParallelTrackingExtensions.PushNewBranch(level1); + var level3 = ParallelTrackingExtensions.PushNewBranch(level2); level2.ShouldStartWith(level1); level3.ShouldStartWith(level2); var l3Splits = level3.Split('.'); @@ -76,7 +76,7 @@ public void Push() context => { context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); - context.CurrentInstance.SetCurrentParallelBranchId(_noBranch.PushNewBranch()); + context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(_noBranch)); context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); }); @@ -96,7 +96,7 @@ public void Push() public void PushAndSetParallelBranch() => RunWithParent( context => { - var pushLevelOnSchedule = _parentLevel.PushNewBranch(); + var pushLevelOnSchedule = ParallelTrackingExtensions.PushNewBranch(_parentLevel); var scheduledInstance = context.CurrentInstance; scheduledInstance.SetCurrentParallelBranchId(pushLevelOnSchedule); var getPushedLevel = scheduledInstance.GetCurrentParallelBranchId(); @@ -109,8 +109,8 @@ public void Push() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch()); - Should.Throw(() => instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch())); + instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(_parentLevel)); + Should.Throw(() => instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel)))); }); [Fact] @@ -118,7 +118,7 @@ public void Push() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch()); + instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); }); [Fact] @@ -126,7 +126,7 @@ public void Push() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch()); + instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); instance.SetCurrentParallelBranchId(_parentLevel); }); @@ -135,15 +135,15 @@ public void Push() context => { var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch().PushNewBranch()); - Should.Throw(() => scheduledInstance.SetCurrentParallelBranchId(_parentLevel.PushNewBranch())); + scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); + Should.Throw(() => scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); }); [Fact] public void ParallelBranchDoesNotLeakToSiblings() => RunWithParent( context => { - context.CurrentInstance.SetCurrentParallelBranchId(context.CurrentInstance.GetCurrentParallelBranchId().PushNewBranch()); + context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(context.CurrentInstance.GetCurrentParallelBranchId())); context.CurrentInstance.GetCurrentParallelBranchId().ShouldNotBe(_parentLevel); context.CurrentInstance.GetCurrentParallelBranchId().ShouldStartWith(_parentLevel); }, diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs index 723f3756..6465d165 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs @@ -28,9 +28,10 @@ private static JsonSerializer Serializer() => new() { Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Auto, - PreserveReferencesHandling = PreserveReferencesHandling.Objects, ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - ObjectCreationHandling = ObjectCreationHandling.Replace + ObjectCreationHandling = ObjectCreationHandling.Replace, + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + ReferenceLoopHandling = ReferenceLoopHandling.Serialize }; } diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index d001d659..6b6b33fc 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -44,8 +44,8 @@ public static void SetCurrentParallelBranchId(this ActivityInstance instance, st props.Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); } - public static string PushNewBranch(this string thisStack) => - $"{thisStack}.{Guid.NewGuid():N}".Trim('.'); + public static string PushNewBranch(string branchId) => + $"{branchId}.{Guid.NewGuid():N}".Trim('.'); private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => new(null, instance, instance.PropertyManager); From 7e2c1a05f3ee8107f29c9a79438c7f747f71b80c Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Fri, 10 May 2024 14:54:47 +0300 Subject: [PATCH 10/17] remove jsonInstanceSerializer project --- src/Test/JsonFileInstanceStore/AsyncResult.cs | 177 ------------- .../JsonFileInstanceStore.cs | 237 ------------------ .../JsonFileInstanceStore.csproj | 1 - .../JsonFileInstanceStore/TypedAsyncResult.cs | 38 --- .../TypedCompletedAsyncResult.cs | 31 --- src/Test/TestCases.Activities/DoWhile.cs | 2 +- .../Flowchart/Execution.cs | 8 +- src/Test/TestCases.Activities/ForEach.cs | 2 +- src/Test/TestCases.Activities/If.cs | 2 +- .../TestCases.Activities/ParallelActivity.cs | 2 +- .../TestCases.Activities/ParallelForEach.cs | 2 +- src/Test/TestCases.Activities/Rethrow.cs | 2 +- src/Test/TestCases.Activities/Sequence.cs | 2 +- .../TestCases.Activities.csproj | 2 +- src/Test/TestCases.Activities/TryCatch.cs | 6 +- .../TestCases.Activities/WhileActivity.cs | 2 +- .../TestCases.Activities/WriteLineActivity.cs | 2 +- .../TestCases.Runtime/Common/RuntimeHelper.cs | 2 +- .../TestCases.Runtime.csproj | 1 - ...WorflowInstanceResumeBookmarkAsyncTests.cs | 17 +- .../WorkflowInstanceAbortTests.cs | 4 +- .../CompiledLocationSerializationTests.cs | 8 +- .../TestCases.Workflows.csproj | 2 +- .../Persistence/AbstractInstanceStore.cs | 94 ++----- .../Persistence/FileInstanceStore.cs | 4 +- .../Persistence/JsonWorkflowSerializer.cs | 17 +- .../Persistence/MemoryInstanceStore.cs | 6 +- src/UiPath.Workflow.sln | 7 - 28 files changed, 75 insertions(+), 605 deletions(-) delete mode 100644 src/Test/JsonFileInstanceStore/AsyncResult.cs delete mode 100644 src/Test/JsonFileInstanceStore/JsonFileInstanceStore.cs delete mode 100644 src/Test/JsonFileInstanceStore/JsonFileInstanceStore.csproj delete mode 100644 src/Test/JsonFileInstanceStore/TypedAsyncResult.cs delete mode 100644 src/Test/JsonFileInstanceStore/TypedCompletedAsyncResult.cs diff --git a/src/Test/JsonFileInstanceStore/AsyncResult.cs b/src/Test/JsonFileInstanceStore/AsyncResult.cs deleted file mode 100644 index 6bd50f27..00000000 --- a/src/Test/JsonFileInstanceStore/AsyncResult.cs +++ /dev/null @@ -1,177 +0,0 @@ -// This file is part of Core WF which is licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -using System; -using System.Diagnostics; -using System.Runtime.ExceptionServices; -using System.Threading; - -namespace JsonFileInstanceStore -{ - /// - /// A generic base class for IAsyncResult implementations - /// that wraps a ManualResetEvent. - /// - internal abstract class FileStoreAsyncResult : IAsyncResult - { - private readonly AsyncCallback _callback; - private readonly object _state; - private bool _completedSynchronously; - private bool _endCalled; - private Exception _exception; - private bool _isCompleted; - private ManualResetEvent _manualResetEvent; - private readonly object _thisLock; - - protected FileStoreAsyncResult(AsyncCallback callback, object state) - { - _callback = callback; - _state = state; - _thisLock = new object(); - } - - public object AsyncState - { - get - { - return _state; - } - } - - public WaitHandle AsyncWaitHandle - { - get - { - if (_manualResetEvent != null) - { - return _manualResetEvent; - } - - lock (ThisLock) - { - if (_manualResetEvent == null) - { - _manualResetEvent = new ManualResetEvent(_isCompleted); - } - } - - return _manualResetEvent; - } - } - - public bool CompletedSynchronously - { - get - { - return _completedSynchronously; - } - } - - public bool IsCompleted - { - get - { - return _isCompleted; - } - } - - private object ThisLock - { - get - { - return _thisLock; - } - } - - // Call this version of complete when your asynchronous operation is complete. This will update the state - // of the operation and notify the callback. - protected void Complete(bool completedSynchronously) - { - if (_isCompleted) - { - // It is incorrect to call Complete twice. - // throw new InvalidOperationException(Resources.AsyncResultAlreadyCompleted); - throw new InvalidOperationException("AsyncResultAlreadyCompleted"); - } - - _completedSynchronously = completedSynchronously; - - if (completedSynchronously) - { - // If we completedSynchronously, then there is no chance that the manualResetEvent was created so - // we do not need to worry about a race condition. - Debug.Assert(_manualResetEvent == null, "No ManualResetEvent should be created for a synchronous AsyncResult."); - _isCompleted = true; - } - else - { - lock (ThisLock) - { - _isCompleted = true; - if (_manualResetEvent != null) - { - _manualResetEvent.Set(); - } - } - } - - // If the callback throws, the callback implementation is incorrect - if (_callback != null) - { - _callback(this); - } - } - - // Call this version of complete if you raise an exception during processing. In addition to notifying - // the callback, it will capture the exception and store it to be thrown during AsyncResult.End. - protected void Complete(bool completedSynchronously, Exception exception) - { - _exception = exception; - Complete(completedSynchronously); - } - - // End should be called when the End function for the asynchronous operation is complete. It - // ensures the asynchronous operation is complete, and does some common validation. - protected static TAsyncResult End(IAsyncResult result) - where TAsyncResult : FileStoreAsyncResult - { - if (result == null) - { - throw new ArgumentNullException("result"); - } - - - if (!(result is TAsyncResult asyncResult)) - { - // throw new ArgumentException(Resources.InvalidAsyncResult); - throw new ArgumentException("InvalidAsyncResult"); - } - - if (asyncResult._endCalled) - { - // throw new InvalidOperationException(Resources.AsyncResultAlreadyEnded); - throw new InvalidOperationException("AsyncResultAlreadyEnded"); - } - - asyncResult._endCalled = true; - - if (!asyncResult._isCompleted) - { - asyncResult.AsyncWaitHandle.WaitOne(); - } - - if (asyncResult._manualResetEvent != null) - { - // was manualResetEvent.Close(); - asyncResult._manualResetEvent.Dispose(); - } - - var exception = asyncResult._exception; - if (exception != null) - { - ExceptionDispatchInfo.Capture(exception).Throw(); - } - return asyncResult; - } - } -} diff --git a/src/Test/JsonFileInstanceStore/JsonFileInstanceStore.cs b/src/Test/JsonFileInstanceStore/JsonFileInstanceStore.cs deleted file mode 100644 index 5d0dae86..00000000 --- a/src/Test/JsonFileInstanceStore/JsonFileInstanceStore.cs +++ /dev/null @@ -1,237 +0,0 @@ -// This file is part of Core WF which is licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -using System.Activities.DurableInstancing; -using System.Activities.Runtime.DurableInstancing; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Xml.Linq; - -namespace JsonFileInstanceStore -{ - public class FileInstanceStore : InstanceStore - { - private readonly string _storeDirectoryPath; - - public static readonly JsonSerializerSettings JsonSerializerSettings = new() - { - TypeNameHandling = TypeNameHandling.Auto, - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - ObjectCreationHandling = ObjectCreationHandling.Replace, - PreserveReferencesHandling = PreserveReferencesHandling.Objects, - ReferenceLoopHandling = ReferenceLoopHandling.Serialize - }; - - public FileInstanceStore(string storeDirectoryPath) - { - _storeDirectoryPath = storeDirectoryPath; - Directory.CreateDirectory(storeDirectoryPath); - } - - public bool KeepInstanceDataAfterCompletion - { - get; - set; - } - - private void DeleteFiles(Guid instanceId) - { - try - { - File.Delete(_storeDirectoryPath + "\\" + instanceId.ToString() + "-InstanceData"); - File.Delete(_storeDirectoryPath + "\\" + instanceId.ToString() + "-InstanceMetadata"); - } - catch (Exception ex) - { - Console.WriteLine("Caught exception trying to delete files for {0}: {1} - {2}", instanceId.ToString(), ex.GetType().ToString(), ex.Message); - } - } - - protected override IAsyncResult BeginTryCommand(InstancePersistenceContext context, InstancePersistenceCommand command, TimeSpan timeout, AsyncCallback callback, object state) - { - if (command is SaveWorkflowCommand saveWorkflowCommand) - { - return new TypedCompletedAsyncResult(SaveWorkflow(context, saveWorkflowCommand), callback, state); - } - else if (command is LoadWorkflowCommand loadWorkflowCommand) - { - return new TypedCompletedAsyncResult(LoadWorkflow(context, loadWorkflowCommand), callback, state); - } - else if (command is CreateWorkflowOwnerCommand createWorkflowOwnerCommand) - { - return new TypedCompletedAsyncResult(CreateWorkflowOwner(context, createWorkflowOwnerCommand), callback, state); - } - else if (command is DeleteWorkflowOwnerCommand deleteWorkflowOwnerCommand) - { - return new TypedCompletedAsyncResult(DeleteWorkflowOwner(context, deleteWorkflowOwnerCommand), callback, state); - } - return new TypedCompletedAsyncResult(false, callback, state); - } - - protected override bool EndTryCommand(IAsyncResult result) => TypedCompletedAsyncResult.End(result); - - private bool SaveWorkflow(InstancePersistenceContext context, SaveWorkflowCommand command) - { - if (context.InstanceVersion == -1) - { - context.BindAcquiredLock(0); - } - - if (command.CompleteInstance) - { - context.CompletedInstance(); - if (!KeepInstanceDataAfterCompletion) - { - DeleteFiles(context.InstanceView.InstanceId); - } - } - else - { - Dictionary instanceData = SerializeablePropertyBagConvertXNameInstanceValue(command.InstanceData); - Dictionary instanceMetadata = SerializeInstanceMetadataConvertXNameInstanceValue(context, command); - - var serializedInstanceData = JsonConvert.SerializeObject(instanceData, Formatting.Indented, JsonSerializerSettings); - File.WriteAllText(_storeDirectoryPath + "\\" + context.InstanceView.InstanceId + "-InstanceData", serializedInstanceData); - - var serializedInstanceMetadata = JsonConvert.SerializeObject(instanceMetadata, Formatting.Indented, JsonSerializerSettings); - File.WriteAllText(_storeDirectoryPath + "\\" + context.InstanceView.InstanceId + "-InstanceMetadata", serializedInstanceMetadata); - - foreach (KeyValuePair property in command.InstanceMetadataChanges) - { - context.WroteInstanceMetadataValue(property.Key, property.Value); - } - - context.PersistedInstance(command.InstanceData); - if (command.CompleteInstance) - { - context.CompletedInstance(); - } - - if (command.UnlockInstance || command.CompleteInstance) - { - context.InstanceHandle.Free(); - } - } - - return true; - } - - private bool LoadWorkflow(InstancePersistenceContext context, LoadWorkflowCommand command) - { - if (command.AcceptUninitializedInstance) - { - return false; - } - - if (context.InstanceVersion == -1) - { - context.BindAcquiredLock(0); - } - - IDictionary instanceData = null; - IDictionary instanceMetadata = null; - - Dictionary serializableInstanceData; - Dictionary serializableInstanceMetadata; - - try - { - var serializedInstanceData = File.ReadAllText(_storeDirectoryPath + "\\" + context.InstanceView.InstanceId + "-InstanceData"); - serializableInstanceData = JsonConvert.DeserializeObject>(serializedInstanceData, JsonSerializerSettings); - - var serializedInstanceMetadata = File.ReadAllText(_storeDirectoryPath + "\\" + context.InstanceView.InstanceId + "-InstanceMetadata"); - serializableInstanceMetadata = JsonConvert.DeserializeObject>(serializedInstanceMetadata, JsonSerializerSettings); - } - catch (Exception) - { - throw; - } - - instanceData = this.DeserializePropertyBagConvertXNameInstanceValue(serializableInstanceData); - instanceMetadata = this.DeserializePropertyBagConvertXNameInstanceValue(serializableInstanceMetadata); - - context.LoadedInstance(InstanceState.Initialized, instanceData, instanceMetadata, null, null); - - return true; - } - - private bool CreateWorkflowOwner(InstancePersistenceContext context, CreateWorkflowOwnerCommand command) - { - Guid instanceOwnerId = Guid.NewGuid(); - context.BindInstanceOwner(instanceOwnerId, instanceOwnerId); - context.BindEvent(HasRunnableWorkflowEvent.Value); - return true; - } - - private bool DeleteWorkflowOwner(InstancePersistenceContext context, DeleteWorkflowOwnerCommand command) - { - return true; - } - - private Dictionary SerializeablePropertyBagConvertXNameInstanceValue(IDictionary source) - { - Dictionary scratch = new Dictionary(); - foreach (KeyValuePair property in source) - { - bool writeOnly = (property.Value.Options & InstanceValueOptions.WriteOnly) != 0; - - if (!writeOnly && !property.Value.IsDeletedValue) - { - scratch.Add(property.Key.ToString(), property.Value); - } - } - - return scratch; - } - - private Dictionary SerializeInstanceMetadataConvertXNameInstanceValue(InstancePersistenceContext context, SaveWorkflowCommand command) - { - Dictionary metadata = null; - - foreach (var property in command.InstanceMetadataChanges) - { - if (!property.Value.Options.HasFlag(InstanceValueOptions.WriteOnly)) - { - if (metadata == null) - { - metadata = new Dictionary(); - // copy current metadata. note that we must get rid of InstanceValue as it is not properly serializeable - foreach (var m in context.InstanceView.InstanceMetadata) - { - metadata.Add(m.Key.ToString(), m.Value); - } - } - - if (metadata.ContainsKey(property.Key.ToString())) - { - if (property.Value.IsDeletedValue) metadata.Remove(property.Key.ToString()); - else metadata[property.Key.ToString()] = property.Value; - } - else - { - if (!property.Value.IsDeletedValue) metadata.Add(property.Key.ToString(), property.Value); - } - } - } - - if (metadata == null) - metadata = new Dictionary(); - - return metadata; - } - - private IDictionary DeserializePropertyBagConvertXNameInstanceValue(Dictionary source) - { - Dictionary destination = new Dictionary(); - - foreach (KeyValuePair property in source) - { - destination.Add(property.Key, property.Value); - } - - return destination; - } - } -} diff --git a/src/Test/JsonFileInstanceStore/JsonFileInstanceStore.csproj b/src/Test/JsonFileInstanceStore/JsonFileInstanceStore.csproj deleted file mode 100644 index a6968989..00000000 --- a/src/Test/JsonFileInstanceStore/JsonFileInstanceStore.csproj +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Test/JsonFileInstanceStore/TypedAsyncResult.cs b/src/Test/JsonFileInstanceStore/TypedAsyncResult.cs deleted file mode 100644 index ca587201..00000000 --- a/src/Test/JsonFileInstanceStore/TypedAsyncResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -// This file is part of Core WF which is licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -using System; - -namespace JsonFileInstanceStore -{ - /// - /// A strongly typed AsyncResult. - /// - /// - internal abstract class TypedAsyncResult : FileStoreAsyncResult - { - private T _data; - - protected TypedAsyncResult(AsyncCallback callback, object state) - : base(callback, state) - { - } - - public T Data - { - get { return _data; } - } - - protected void Complete(T data, bool completedSynchronously) - { - _data = data; - Complete(completedSynchronously); - } - - public static T End(IAsyncResult result) - { - TypedAsyncResult typedResult = FileStoreAsyncResult.End>(result); - return typedResult.Data; - } - } -} diff --git a/src/Test/JsonFileInstanceStore/TypedCompletedAsyncResult.cs b/src/Test/JsonFileInstanceStore/TypedCompletedAsyncResult.cs deleted file mode 100644 index 5805de01..00000000 --- a/src/Test/JsonFileInstanceStore/TypedCompletedAsyncResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -// This file is part of Core WF which is licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -using System; - -namespace JsonFileInstanceStore -{ - /// - /// A strongly typed AsyncResult that completes as soon as it is instantiated. - /// - /// - internal class TypedCompletedAsyncResult : TypedAsyncResult - { - public TypedCompletedAsyncResult(T data, AsyncCallback callback, object state) - : base(callback, state) - { - Complete(data, true); - } - - public new static T End(IAsyncResult result) - { - - if (!(result is TypedCompletedAsyncResult completedResult)) - { - throw new ArgumentException("InvalidAsyncResult"); - } - - return TypedAsyncResult.End(completedResult); - } - } -} diff --git a/src/Test/TestCases.Activities/DoWhile.cs b/src/Test/TestCases.Activities/DoWhile.cs index 136f8b91..208271ee 100644 --- a/src/Test/TestCases.Activities/DoWhile.cs +++ b/src/Test/TestCases.Activities/DoWhile.cs @@ -813,7 +813,7 @@ public void DoWhilePersisted() HintIterationCount = 1 }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(doWhile, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/Flowchart/Execution.cs b/src/Test/TestCases.Activities/Flowchart/Execution.cs index 51d7c12d..2acb2943 100644 --- a/src/Test/TestCases.Activities/Flowchart/Execution.cs +++ b/src/Test/TestCases.Activities/Flowchart/Execution.cs @@ -150,7 +150,7 @@ public void PersistFlowchart() flowchart.AddLink(writeLine1, blocking); flowchart.AddLink(blocking, writeLine2); - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(flowchart, null, jsonStore, PersistableIdleAction.None)) { @@ -556,7 +556,7 @@ public void UnloadFlowchartWhileExecutingFlowConditionalCondition() new TestWriteLine("False", "False Action")); - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(System.Environment.CurrentDirectory); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(System.Environment.CurrentDirectory); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(flowchart, null, jsonStore, PersistableIdleAction.Unload)) { @@ -604,7 +604,7 @@ public void UnloadFlowchartWhileExecutingFlowSwitchExpression() flowchart.AddSwitchLink(writeStart, cases, hints, expressionActivity, new TestWriteLine("Default", "Will not execute")); - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(flowchart, null, jsonStore, PersistableIdleAction.Unload)) { @@ -693,7 +693,7 @@ public void UnloadFlowchartWhileExecutingFlowStep() flowchart.AddStartLink(blocking); - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(flowchart, null, jsonStore, PersistableIdleAction.Unload)) { diff --git a/src/Test/TestCases.Activities/ForEach.cs b/src/Test/TestCases.Activities/ForEach.cs index 1ada7828..f375d04d 100644 --- a/src/Test/TestCases.Activities/ForEach.cs +++ b/src/Test/TestCases.Activities/ForEach.cs @@ -1064,7 +1064,7 @@ public void ForeachWithPersistence() Values = intArray }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(foreachAct, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/If.cs b/src/Test/TestCases.Activities/If.cs index 791b2f1c..043230f2 100644 --- a/src/Test/TestCases.Activities/If.cs +++ b/src/Test/TestCases.Activities/If.cs @@ -913,7 +913,7 @@ public void IfInWhileWithPersistence() HintIterationCount = 1 }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(whileAct, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/ParallelActivity.cs b/src/Test/TestCases.Activities/ParallelActivity.cs index 2d336357..f7b82c69 100644 --- a/src/Test/TestCases.Activities/ParallelActivity.cs +++ b/src/Test/TestCases.Activities/ParallelActivity.cs @@ -339,7 +339,7 @@ public void PersistWithinBranch() } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(parallel, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/ParallelForEach.cs b/src/Test/TestCases.Activities/ParallelForEach.cs index 051d8af7..aa157bbe 100644 --- a/src/Test/TestCases.Activities/ParallelForEach.cs +++ b/src/Test/TestCases.Activities/ParallelForEach.cs @@ -416,7 +416,7 @@ public void PersistParallelForEach() } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(parallelForEach, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/Rethrow.cs b/src/Test/TestCases.Activities/Rethrow.cs index 507420f1..6c5fc972 100644 --- a/src/Test/TestCases.Activities/Rethrow.cs +++ b/src/Test/TestCases.Activities/Rethrow.cs @@ -333,7 +333,7 @@ public void PersistAfterCatchBeforeRethrow() } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(root, null, jsonStore, System.Activities.PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/Sequence.cs b/src/Test/TestCases.Activities/Sequence.cs index e49db405..e0e644c7 100644 --- a/src/Test/TestCases.Activities/Sequence.cs +++ b/src/Test/TestCases.Activities/Sequence.cs @@ -634,7 +634,7 @@ public void CancelSequenceInTheMiddle() } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(seq, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/TestCases.Activities.csproj b/src/Test/TestCases.Activities/TestCases.Activities.csproj index 59a94743..830dc472 100644 --- a/src/Test/TestCases.Activities/TestCases.Activities.csproj +++ b/src/Test/TestCases.Activities/TestCases.Activities.csproj @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/Test/TestCases.Activities/TryCatch.cs b/src/Test/TestCases.Activities/TryCatch.cs index 39076eaa..87edff88 100644 --- a/src/Test/TestCases.Activities/TryCatch.cs +++ b/src/Test/TestCases.Activities/TryCatch.cs @@ -929,7 +929,7 @@ public void PersistInCatch() Catches = { { new TestCatch { Body = blocking } } } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(tcf, null, jsonStore, PersistableIdleAction.None)) { @@ -978,7 +978,7 @@ public void PersistInTry() Finally = new TestWriteLine("Finally", "Finally") }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(tryCatch, null, jsonStore, PersistableIdleAction.None)) { @@ -1008,7 +1008,7 @@ public void PersistInFinally() Finally = new TestSequence { Activities = { blocking, new TestWriteLine("Finally", "Finally") } } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime testWorkflowRuntime = TestRuntime.CreateTestWorkflowRuntime(tcf, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/WhileActivity.cs b/src/Test/TestCases.Activities/WhileActivity.cs index 0902e434..1fe3e6f4 100644 --- a/src/Test/TestCases.Activities/WhileActivity.cs +++ b/src/Test/TestCases.Activities/WhileActivity.cs @@ -712,7 +712,7 @@ public void WhilePersisted() HintIterationCount = 1 }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(whileAct, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Activities/WriteLineActivity.cs b/src/Test/TestCases.Activities/WriteLineActivity.cs index eed42a72..7d555334 100644 --- a/src/Test/TestCases.Activities/WriteLineActivity.cs +++ b/src/Test/TestCases.Activities/WriteLineActivity.cs @@ -522,7 +522,7 @@ public void PersistenceToWriteLineActivity() TestBlockingActivity blocking = new TestBlockingActivity("BlockingActivity"); - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime workflow = TestRuntime.CreateTestWorkflowRuntime(writeLine1, null, jsonStore, PersistableIdleAction.None)) { diff --git a/src/Test/TestCases.Runtime/Common/RuntimeHelper.cs b/src/Test/TestCases.Runtime/Common/RuntimeHelper.cs index 0f47eb83..665568ff 100644 --- a/src/Test/TestCases.Runtime/Common/RuntimeHelper.cs +++ b/src/Test/TestCases.Runtime/Common/RuntimeHelper.cs @@ -392,7 +392,7 @@ private static WorkflowApplicationInstance GetADummyWorkflowApplicationInstance( }; WorkflowApplicationInstance waInstance; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); using (TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(wfDefinition, null, jsonStore, PersistableIdleAction.Unload)) { diff --git a/src/Test/TestCases.Runtime/TestCases.Runtime.csproj b/src/Test/TestCases.Runtime/TestCases.Runtime.csproj index 430acdd3..b928f79d 100644 --- a/src/Test/TestCases.Runtime/TestCases.Runtime.csproj +++ b/src/Test/TestCases.Runtime/TestCases.Runtime.csproj @@ -1,6 +1,5 @@  - diff --git a/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs b/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs index 414343d4..6488215c 100644 --- a/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs +++ b/src/Test/TestCases.Runtime/WorflowInstanceResumeBookmarkAsyncTests.cs @@ -11,6 +11,7 @@ using Test.Common.TestObjects.Utilities; using Test.Common.TestObjects.Utilities.Validation; using TestCases.Runtime.Common.Activities; +using WorkflowApplicationTestExtensions.Persistence; using Xunit; namespace TestCases.Runtime.WorkflowInstanceTest; public class WorflowInstanceResumeBookmarkAsyncTests @@ -51,7 +52,7 @@ public static void TestOperationsResumeBookmarkCallback(int operationId) } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(testSequence, null, jsonStore, PersistableIdleAction.Unload); workflowRuntime.ExecuteWorkflow(); workflowRuntime.WaitForActivityStatusChange("WaitActivity", TestActivityInstanceState.Executing); @@ -171,7 +172,7 @@ private static void TestInstanceOperationFromResumeBookmarkCallback(int operatio string message = ""; //Execute Workflow - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); // using PersistableIdleAction.None here because the idle unload was racing with the resume bookmark after the wait for the BeforeWait trace. TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(testSequence, null, jsonStore, PersistableIdleAction.None); workflowRuntime.ExecuteWorkflow(); @@ -335,13 +336,17 @@ public static void TestPersistDuringResumeBookmark() } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + var jsonStore = new MemoryInstanceStore(); + var unloadEvent = new ManualResetEvent(false); TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(testSequence, null, jsonStore, PersistableIdleAction.Unload); + workflowRuntime.OnWorkflowUnloaded += (_, __) => unloadEvent.Set(); + workflowRuntime.ExecuteWorkflow(); workflowRuntime.WaitForActivityStatusChange("WaitActivity", TestActivityInstanceState.Executing); TestTraceManager.Instance.AddTrace(workflowRuntime.CurrentWorkflowInstanceId, new SynchronizeTrace(WaitMessage)); SynchronizeTrace.Trace(workflowRuntime.CurrentWorkflowInstanceId, WaitMessage); + unloadEvent.Reset(); if (isSync) { workflowRuntime.ResumeBookMark("Read", 9999); @@ -357,7 +362,7 @@ public static void TestPersistDuringResumeBookmark() } workflowRuntime.WaitForActivityStatusChange("PersistBookmark", TestActivityInstanceState.Executing); - workflowRuntime.WaitForUnloaded(1); + unloadEvent.WaitOne(); workflowRuntime.LoadWorkflow(); workflowRuntime.ResumeBookMark("PersistBookmark", "Yes"); workflowRuntime.WaitForCompletion(false); @@ -376,7 +381,7 @@ public static void TestResumeWithDelay() }, } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(testSequence, null, jsonStore, PersistableIdleAction.Unload); workflowRuntime.ExecuteWorkflow(); workflowRuntime.PersistWorkflow(); @@ -389,7 +394,7 @@ public static void TestResumeWithDelay() public static void TestNoPersistSerialization() { TestSequence testSequence = new() { Activities = { new TestNoPersist() }}; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); TestWorkflowRuntime workflowRuntime = TestRuntime.CreateTestWorkflowRuntime(testSequence, null, jsonStore, PersistableIdleAction.Unload); workflowRuntime.ExecuteWorkflow(); workflowRuntime.PersistWorkflow(); diff --git a/src/Test/TestCases.Runtime/WorkflowInstanceAbortTests.cs b/src/Test/TestCases.Runtime/WorkflowInstanceAbortTests.cs index e136997b..d9d35b9d 100644 --- a/src/Test/TestCases.Runtime/WorkflowInstanceAbortTests.cs +++ b/src/Test/TestCases.Runtime/WorkflowInstanceAbortTests.cs @@ -218,7 +218,7 @@ public void TestAbortLoad() } }; - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(sequence, null, jsonStore, PersistableIdleAction.Persist); runtime.ExecuteWorkflow(); runtime.WaitForActivityStatusChange("Read", TestActivityInstanceState.Executing); @@ -277,7 +277,7 @@ public void TestResumeBookmarkCallback() parallel.Branches.Add(sequence); } - JsonFileInstanceStore.FileInstanceStore jsonStore = new JsonFileInstanceStore.FileInstanceStore(".\\~"); + WorkflowApplicationTestExtensions.Persistence.FileInstanceStore jsonStore = new WorkflowApplicationTestExtensions.Persistence.FileInstanceStore(".\\~"); TestWorkflowRuntime runtime = TestRuntime.CreateTestWorkflowRuntime(parallel, null, jsonStore, PersistableIdleAction.Persist); runtime.ExecuteWorkflow(); diff --git a/src/Test/TestCases.Workflows/CompiledLocationSerializationTests.cs b/src/Test/TestCases.Workflows/CompiledLocationSerializationTests.cs index 2109fd9a..527aad58 100644 --- a/src/Test/TestCases.Workflows/CompiledLocationSerializationTests.cs +++ b/src/Test/TestCases.Workflows/CompiledLocationSerializationTests.cs @@ -1,5 +1,4 @@ -using JsonFileInstanceStore; -using Newtonsoft.Json; +using Newtonsoft.Json; using Shouldly; using System.Activities; using System.Activities.Statements; @@ -7,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; +using WorkflowApplicationTestExtensions.Persistence; using Xunit; namespace TestCases.Workflows @@ -35,8 +35,8 @@ static CompiledLocation SerializeAndDeserialize(CompiledLocation { if (useJsonSerialization) { - var json = JsonConvert.SerializeObject(compiledLocation, FileInstanceStore.JsonSerializerSettings); - return JsonConvert.DeserializeObject>(json, FileInstanceStore.JsonSerializerSettings); + var json = JsonConvert.SerializeObject(compiledLocation, JsonWorkflowSerializer.SerializerSettings()); + return JsonConvert.DeserializeObject>(json, JsonWorkflowSerializer.SerializerSettings()); } else { diff --git a/src/Test/TestCases.Workflows/TestCases.Workflows.csproj b/src/Test/TestCases.Workflows/TestCases.Workflows.csproj index 7c07f3b0..139aa560 100644 --- a/src/Test/TestCases.Workflows/TestCases.Workflows.csproj +++ b/src/Test/TestCases.Workflows/TestCases.Workflows.csproj @@ -2,7 +2,7 @@ - + diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs index dd34b541..29f60d33 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs @@ -30,7 +30,7 @@ public interface IWorkflowSerializer public void SaveWorkflowInstance(XInstanceDictionary workflowInstanceState, Stream destinationStream); } -static class WorkflowSerializerHelpers +public static class WorkflowSerializerHelpers { public static InstanceDictionary ToSave(this XInstanceDictionary source) => source .Where(property => !property.Value.Options.HasFlag(InstanceValueOptions.WriteOnly) && !property.Value.IsDeletedValue) @@ -45,66 +45,11 @@ public abstract class AbstractInstanceStore(IWorkflowSerializer instanceSerializ private readonly Guid _lockId = Guid.NewGuid(); private readonly IWorkflowSerializer _instanceSerializer = instanceSerializer; - private class StreamWrapperWithDisposeEvent : Stream - { - private readonly Stream _stream; - public Action OnDispose { get; init; } - private readonly Guid _instanceId; - - public StreamWrapperWithDisposeEvent(Stream stream, Guid instanceId) - { - _stream = stream; - _instanceId = instanceId; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _stream.Dispose(); - OnDispose?.Invoke(_instanceId, _stream); - } - - public override bool CanRead => _stream.CanRead; - - public override bool CanSeek => _stream.CanSeek; - - public override bool CanWrite => _stream.CanWrite; - - public override long Length => _stream.Length; - - public override long Position { get => _stream.Position; set => _stream.Position = value; } - - public override void Flush() - { - _stream.Flush(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _stream.Read(buffer, offset, count); - } + protected virtual void OnLoadDone(Guid instanceId, Stream stream) { } + protected virtual void OnSaveDone(Guid instanceId, Stream stream) { } - public override long Seek(long offset, SeekOrigin origin) - { - return _stream.Seek(offset, origin); - } - - public override void SetLength(long value) - { - _stream.SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _stream.Write(buffer, offset, count); - } - } - - protected virtual void OnReadStreamDisposed(Guid instanceId, Stream stream) { } - protected virtual void OnWriteStreamDisposed(Guid instanceId, Stream stream) { } - - protected abstract Task GetReadStream(Guid instanceId); - protected abstract Task GetWriteStream(Guid instanceId); + protected abstract Task GetLoadStream(Guid instanceId); + protected abstract Task GetSaveStream(Guid instanceId); protected sealed override IAsyncResult BeginTryCommand(InstancePersistenceContext context, InstancePersistenceCommand command, TimeSpan timeout, AsyncCallback callback, object state) { @@ -127,10 +72,7 @@ async Task TryCommandAndYield() } protected sealed override bool EndTryCommand(IAsyncResult result) - { - var task = (Task)result; - return task.Result; - } + => ApmAsyncFactory.ToEnd(result); private async Task TryCommandAsync(InstancePersistenceContext context, InstancePersistenceCommand command) { @@ -167,27 +109,29 @@ private async Task TryCommandAsync(InstancePersistenceContext context, Ins private async Task LoadWorkflow(InstancePersistenceContext context) { - var originalStream = await GetReadStream(context.InstanceView.InstanceId); - using var stream = new StreamWrapperWithDisposeEvent(originalStream, context.InstanceView.InstanceId) - { - OnDispose = OnReadStreamDisposed - }; - context.LoadedInstance(InstanceState.Initialized, _instanceSerializer.LoadWorkflowInstance(stream), null, null, null); + var originalStream = await GetLoadStream(context.InstanceView.InstanceId); + var deserializedInstanceData = _instanceSerializer.LoadWorkflowInstance(originalStream); + context.LoadedInstance(InstanceState.Initialized, deserializedInstanceData, null, null, null); + OnLoadDone(context.InstanceView.InstanceId, originalStream); } private async Task SaveWorkflow(InstancePersistenceContext context, SaveWorkflowCommand command) { - var originalStream = await GetWriteStream(context.InstanceView.InstanceId); - using var stream = new StreamWrapperWithDisposeEvent(originalStream, context.InstanceView.InstanceId) + if (context.InstanceVersion == -1) { - OnDispose = OnWriteStreamDisposed - }; - _instanceSerializer.SaveWorkflowInstance(command.InstanceData, stream); + context.BindAcquiredLock(0); + } + + using var originalStream = await GetSaveStream(context.InstanceView.InstanceId); + _instanceSerializer.SaveWorkflowInstance(command.InstanceData, originalStream); + context.PersistedInstance(command.InstanceData); + OnSaveDone(context.InstanceView.InstanceId, originalStream); } private void CreateWorkflowOwner(InstancePersistenceContext context) { context.BindInstanceOwner(_storageInstanceId, _lockId); + context.BindEvent(HasRunnableWorkflowEvent.Value); } private void CreateWorkflowOwnerWithIdentity(InstancePersistenceContext context) diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs index c7301e3a..3b1712a9 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/FileInstanceStore.cs @@ -16,12 +16,12 @@ public FileInstanceStore(IWorkflowSerializer workflowSerializer, string storeDir Directory.CreateDirectory(storeDirectoryPath); } - protected override Task GetReadStream(Guid instanceId) + protected override Task GetLoadStream(Guid instanceId) { return Task.FromResult(File.OpenRead(GetFilePath(instanceId))); } - protected override Task GetWriteStream(Guid instanceId) + protected override Task GetSaveStream(Guid instanceId) { string filePath = GetFilePath(instanceId); File.Delete(filePath); diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs index 6465d165..0125b020 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs @@ -24,7 +24,21 @@ void IWorkflowSerializer.SaveWorkflowInstance(XInstanceDictionary workflowInstan Serializer().Serialize(writer, workflowInstanceState.ToSave()); writer.Flush(); } - private static JsonSerializer Serializer() => new() + private static JsonSerializer Serializer() + { + var settings = SerializerSettings(); + return new() + { + Formatting = Formatting.Indented, + TypeNameHandling = settings.TypeNameHandling, + ConstructorHandling = settings.ConstructorHandling, + ObjectCreationHandling = settings.ObjectCreationHandling, + PreserveReferencesHandling = settings.PreserveReferencesHandling, + ReferenceLoopHandling = settings.ReferenceLoopHandling + }; + } + + public static JsonSerializerSettings SerializerSettings() => new() { Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Auto, @@ -33,5 +47,4 @@ private static JsonSerializer Serializer() => new() PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize }; - } diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs index 06bc27bc..bd08e259 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/MemoryInstanceStore.cs @@ -12,16 +12,16 @@ public class MemoryInstanceStore(IWorkflowSerializer workflowSerializer) : Abstr public MemoryInstanceStore() : this(new JsonWorkflowSerializer()) { } - protected override void OnReadStreamDisposed(Guid instanceId, Stream stream) + protected override void OnLoadDone(Guid instanceId, Stream stream) => _cache.Remove(instanceId, out _); - protected override Task GetReadStream(Guid instanceId) + protected override Task GetLoadStream(Guid instanceId) { _cache[instanceId].TryGetBuffer(out var buffer); return Task.FromResult(new MemoryStream(buffer.Array, buffer.Offset, buffer.Count)); } - protected override Task GetWriteStream(Guid instanceId) + protected override Task GetSaveStream(Guid instanceId) => Task.FromResult(_cache[instanceId] = new MemoryStream()); } diff --git a/src/UiPath.Workflow.sln b/src/UiPath.Workflow.sln index ac5edfeb..360d70fa 100644 --- a/src/UiPath.Workflow.sln +++ b/src/UiPath.Workflow.sln @@ -13,8 +13,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCases.Activities", "Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCases.Runtime", "Test\TestCases.Runtime\TestCases.Runtime.csproj", "{3BBAAB5F-5EBB-409C-98D4-7693D4C545E0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonFileInstanceStore", "Test\JsonFileInstanceStore\JsonFileInstanceStore.csproj", "{A02ADBAD-8E07-479D-946B-93591EDC487E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCases.Workflows", "Test\TestCases.Workflows\TestCases.Workflows.csproj", "{36AE306E-FDB2-41A4-861F-67112AA5CB7B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImperativeTestCases", "Test\ImperativeTestCases\ImperativeTestCases.csproj", "{A31AC815-D67E-46DD-920F-7D9B5CC63932}" @@ -76,10 +74,6 @@ Global {3BBAAB5F-5EBB-409C-98D4-7693D4C545E0}.Debug|Any CPU.Build.0 = Debug|Any CPU {3BBAAB5F-5EBB-409C-98D4-7693D4C545E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {3BBAAB5F-5EBB-409C-98D4-7693D4C545E0}.Release|Any CPU.Build.0 = Release|Any CPU - {A02ADBAD-8E07-479D-946B-93591EDC487E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A02ADBAD-8E07-479D-946B-93591EDC487E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A02ADBAD-8E07-479D-946B-93591EDC487E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A02ADBAD-8E07-479D-946B-93591EDC487E}.Release|Any CPU.Build.0 = Release|Any CPU {36AE306E-FDB2-41A4-861F-67112AA5CB7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {36AE306E-FDB2-41A4-861F-67112AA5CB7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {36AE306E-FDB2-41A4-861F-67112AA5CB7B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -132,7 +126,6 @@ Global {E73908B0-BC1A-4CB1-BD6A-35790B1815D6} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} {E57F197D-6102-44AF-B5DA-478AEF7F7340} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} {3BBAAB5F-5EBB-409C-98D4-7693D4C545E0} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} - {A02ADBAD-8E07-479D-946B-93591EDC487E} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} {36AE306E-FDB2-41A4-861F-67112AA5CB7B} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} {A31AC815-D67E-46DD-920F-7D9B5CC63932} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} {B464A1B3-7186-42C4-809D-2E28F25986E7} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C} From 782e10a7699e30c307f19bfd9c47d5b1ab17a163 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Tue, 14 May 2024 14:35:20 +0300 Subject: [PATCH 11/17] fix persistence --- .../Persistence/AbstractInstanceStore.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs index 29f60d33..1829c1cb 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs @@ -117,14 +117,8 @@ private async Task LoadWorkflow(InstancePersistenceContext context) private async Task SaveWorkflow(InstancePersistenceContext context, SaveWorkflowCommand command) { - if (context.InstanceVersion == -1) - { - context.BindAcquiredLock(0); - } - using var originalStream = await GetSaveStream(context.InstanceView.InstanceId); _instanceSerializer.SaveWorkflowInstance(command.InstanceData, originalStream); - context.PersistedInstance(command.InstanceData); OnSaveDone(context.InstanceView.InstanceId, originalStream); } From aabb59a8cffa1af05bcb78818e2c3ebcf39ed007 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Tue, 14 May 2024 15:08:33 +0300 Subject: [PATCH 12/17] review comments --- .../TestCases.Runtime/ParallelBranchTests.cs | 36 +++++++++---------- .../DataContractWorkflowSerializer.cs | 6 ---- .../Persistence/Resources.cs | 9 ----- .../WorkflowApplicationTestExtensions.cs | 5 +-- .../ParallelTrackingExtensions.cs | 4 +-- 5 files changed, 21 insertions(+), 39 deletions(-) delete mode 100644 src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index 17e9aefd..319bc1db 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -50,11 +50,11 @@ void SetParent(CodeActivityContext context) } [Fact] - public void Push() + public void GenerateChildParallelBranchId() { - var level1 = ParallelTrackingExtensions.PushNewBranch(null); - var level2 = ParallelTrackingExtensions.PushNewBranch(level1); - var level3 = ParallelTrackingExtensions.PushNewBranch(level2); + var level1 = ParallelTrackingExtensions.GenerateChildParallelBranchId(null); + var level2 = ParallelTrackingExtensions.GenerateChildParallelBranchId(level1); + var level3 = ParallelTrackingExtensions.GenerateChildParallelBranchId(level2); level2.ShouldStartWith(level1); level3.ShouldStartWith(level2); var l3Splits = level3.Split('.'); @@ -76,7 +76,7 @@ public void Push() context => { context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); - context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(_noBranch)); + context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_noBranch)); context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); }); @@ -93,10 +93,10 @@ public void Push() }); [Fact] - public void PushAndSetParallelBranch() => RunWithParent( + public void Generate_Set_And_Get_BranchId() => RunWithParent( context => { - var pushLevelOnSchedule = ParallelTrackingExtensions.PushNewBranch(_parentLevel); + var pushLevelOnSchedule = ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel); var scheduledInstance = context.CurrentInstance; scheduledInstance.SetCurrentParallelBranchId(pushLevelOnSchedule); var getPushedLevel = scheduledInstance.GetCurrentParallelBranchId(); @@ -105,45 +105,45 @@ public void Push() }); [Fact] - public void UnparentedPushFails() => RunWithParent( + public void Unrelated_Set_Fails() => RunWithParent( context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(_parentLevel)); - Should.Throw(() => instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel)))); + instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)); + Should.Throw(() => instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)))); }); [Fact] - public void DoublePush() => RunWithParent( + public void Set_Descendant_level2() => RunWithParent( context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); + instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); }); [Fact] - public void DoublePop() => RunWithParent( + public void Restore_Ancestor_level2() => RunWithParent( context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); + instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); instance.SetCurrentParallelBranchId(_parentLevel); }); [Fact] - public void UnparentedPopFails() => RunWithParent( + public void Unrelated_Restore_Fails() => RunWithParent( context => { var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); - Should.Throw(() => scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(_parentLevel))); + scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); + Should.Throw(() => scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); }); [Fact] public void ParallelBranchDoesNotLeakToSiblings() => RunWithParent( context => { - context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.PushNewBranch(context.CurrentInstance.GetCurrentParallelBranchId())); + context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(context.CurrentInstance.GetCurrentParallelBranchId())); context.CurrentInstance.GetCurrentParallelBranchId().ShouldNotBe(_parentLevel); context.CurrentInstance.GetCurrentParallelBranchId().ShouldStartWith(_parentLevel); }, diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs index 402235fd..a3751d4e 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs @@ -66,7 +66,6 @@ internal sealed class SerializationSurrogateProvider : ISerializationSurrogatePr { private const string FaultWrapperTypeName = "System.Activities.Runtime.FaultCallbackWrapper"; private const string CompletionWrapperTypeName = "System.Activities.Runtime.ActivityCompletionCallbackWrapper"; - private const string NewtonsoftUnserializableNamespace = "Newtonsoft.Json.Linq"; private static bool IsWrapperType(Type type) => type.FullName == FaultWrapperTypeName || type.FullName == CompletionWrapperTypeName; public Type GetSurrogateType(Type type) => IsWrapperType(type) ? typeof(SurrogateWrapper) : null; @@ -75,11 +74,6 @@ public object GetObjectToSerialize(object obj, Type targetType) { var typeToSerialize = obj.GetType(); System.Diagnostics.Trace.TraceInformation($"TypeToSerialize = {typeToSerialize.FullName}"); - //to be removed after .NET8 upgrade ROBO-2615 - if (typeToSerialize.FullName.Contains(NewtonsoftUnserializableNamespace)) - { - throw new InvalidDataContractException(string.Format(Resources.NewtonsoftTypesSerializationError, NewtonsoftUnserializableNamespace, typeToSerialize.FullName)); - } if (!IsWrapperType(typeToSerialize)) { diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs deleted file mode 100644 index de6b1af1..00000000 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/Resources.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace WorkflowApplicationTestExtensions.Persistence -{ - internal class Resources - { - internal static readonly string NewtonsoftTypesSerializationError = "NewtonsoftTypesSerializationError"; - } -} \ No newline at end of file diff --git a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs index 10781c5b..34d6cc0e 100644 --- a/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs +++ b/src/Test/WorkflowApplicationTestExtensions/WorkflowApplicationTestExtensions.cs @@ -88,8 +88,7 @@ public static WorkflowApplicationResult RunUntilCompletion(this WorkflowApplicat } private static WorkflowApplication CloneWorkflowApplication(WorkflowApplication application) - { - var clone = new WorkflowApplication(application.WorkflowDefinition, application.DefinitionIdentity) + => new(application.WorkflowDefinition, application.DefinitionIdentity) { Aborted = application.Aborted, Completed = application.Completed, @@ -97,6 +96,4 @@ private static WorkflowApplication CloneWorkflowApplication(WorkflowApplication Unloaded = application.Unloaded, InstanceStore = application.InstanceStore, }; - return clone; - } } diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index 6b6b33fc..2fcd71b9 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -14,7 +14,7 @@ public static class ParallelTrackingExtensions public static ActivityInstance MarkNewParallelBranch(this ActivityInstance instance) { var parentId = instance.GetCurrentParallelBranchId(); - instance.SetCurrentParallelBranchId(PushNewBranch(parentId)); + instance.SetCurrentParallelBranchId(GenerateChildParallelBranchId(parentId)); return instance; } @@ -44,7 +44,7 @@ public static void SetCurrentParallelBranchId(this ActivityInstance instance, st props.Add(BranchIdPropertyName, branchId, skipValidations: true, onlyVisibleToPublicChildren: false); } - public static string PushNewBranch(string branchId) => + public static string GenerateChildParallelBranchId(string branchId) => $"{branchId}.{Guid.NewGuid():N}".Trim('.'); private static ExecutionProperties GetExecutionProperties(ActivityInstance instance) => From 24fa7a3b9b1afc2477121e88970d9d811b4f5abe Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Tue, 14 May 2024 17:43:14 +0300 Subject: [PATCH 13/17] review comments2 --- .../TestCases.Runtime/ParallelBranchTests.cs | 82 +++++++++-------- .../DataContractWorkflowSerializer.cs | 89 +------------------ .../Persistence/JsonWorkflowSerializer.cs | 2 +- .../ParallelTrackingExtensions.cs | 14 +-- 4 files changed, 55 insertions(+), 132 deletions(-) diff --git a/src/Test/TestCases.Runtime/ParallelBranchTests.cs b/src/Test/TestCases.Runtime/ParallelBranchTests.cs index 319bc1db..3bcbbd2a 100644 --- a/src/Test/TestCases.Runtime/ParallelBranchTests.cs +++ b/src/Test/TestCases.Runtime/ParallelBranchTests.cs @@ -2,6 +2,7 @@ using System; using System.Activities; using System.Activities.ParallelTracking; +using System.Activities.Statements; using System.Linq; using WorkflowApplicationTestExtensions; using WorkflowApplicationTestExtensions.Persistence; @@ -30,22 +31,24 @@ protected override void Execute(CodeActivityContext context) readonly string _noBranch = default; private void Run(params Action[] onExecute) + => Run(Suspendable(onExecute)); + private SuspendingWrapper Suspendable(params Action[] onExecute) + => new(onExecute.Select(c => new TestCodeActivity(c))); + + private void Run(Activity activity) + => new WorkflowApplication(activity) { - new WorkflowApplication(new SuspendingWrapper(onExecute.Select(c => new TestCodeActivity(c)))) - { - InstanceStore = new MemoryInstanceStore(Serializer) - }.RunUntilCompletion(); - } + InstanceStore = new MemoryInstanceStore(Serializer) + }.RunUntilCompletion(); private void RunWithParent(params Action[] onExecute) { - Run([SetParent, .. onExecute]); - void SetParent(CodeActivityContext context) + Run(new Parallel() { Branches = { Suspendable([SaveParent, .. onExecute]) } }); + void SaveParent(CodeActivityContext context) { - var parent = context.CurrentInstance.Parent; - parent.GetCurrentParallelBranchId().ShouldBe(_noBranch); - parent.MarkNewParallelBranch(); - _parentLevel = parent.GetCurrentParallelBranchId(); + _parentLevel = context.CurrentInstance.Parent.GetParallelBranchId(); + _parentLevel.ShouldNotBeNullOrEmpty(); + context.CurrentInstance.GetParallelBranchId().ShouldBe(_parentLevel); } } @@ -66,19 +69,19 @@ public void GenerateChildParallelBranchId() public void SetToNullWhenNull() => Run( context => { - context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); - context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); - context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); + context.CurrentInstance.SetParallelBranchId(_noBranch); + context.CurrentInstance.SetParallelBranchId(_noBranch); + context.CurrentInstance.GetParallelBranchId().ShouldBeNull(); }); [Fact] public void SetToNullWhenNotNull() => Run( context => { - context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); - context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_noBranch)); - context.CurrentInstance.SetCurrentParallelBranchId(_noBranch); - context.CurrentInstance.GetCurrentParallelBranchId().ShouldBeNull(); + context.CurrentInstance.SetParallelBranchId(_noBranch); + context.CurrentInstance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_noBranch)); + context.CurrentInstance.SetParallelBranchId(_noBranch); + context.CurrentInstance.GetParallelBranchId().ShouldBeNull(); }); @@ -87,7 +90,7 @@ public void GenerateChildParallelBranchId() context => { var branchId = context.GetCurrentParallelBranchId(); - var currentBranch = context.CurrentInstance.GetCurrentParallelBranchId(); + var currentBranch = context.CurrentInstance.GetParallelBranchId(); currentBranch.ShouldBe(branchId); currentBranch.ShouldBe(_parentLevel); }); @@ -96,12 +99,12 @@ public void GenerateChildParallelBranchId() public void Generate_Set_And_Get_BranchId() => RunWithParent( context => { - var pushLevelOnSchedule = ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel); - var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranchId(pushLevelOnSchedule); - var getPushedLevel = scheduledInstance.GetCurrentParallelBranchId(); - getPushedLevel.ShouldBe(pushLevelOnSchedule); - scheduledInstance.GetCurrentParallelBranchId().ShouldBe(pushLevelOnSchedule); + var childBranchId = ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel); + var instance = context.CurrentInstance; + instance.SetParallelBranchId(childBranchId); + var getPushedLevel = instance.GetParallelBranchId(); + getPushedLevel.ShouldBe(childBranchId); + instance.GetParallelBranchId().ShouldBe(childBranchId); }); [Fact] @@ -109,25 +112,28 @@ public void GenerateChildParallelBranchId() context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)); - Should.Throw(() => instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)))); + instance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)); + Should.Throw(() => instance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)))); }); [Fact] - public void Set_Descendant_level2() => RunWithParent( + public void Set_Descendant() => RunWithParent( context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); + var secondLevelDecendentBranch = ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel)); + instance.SetParallelBranchId(secondLevelDecendentBranch); + instance.GetParallelBranchId().ShouldBe(secondLevelDecendentBranch); }); [Fact] - public void Restore_Ancestor_level2() => RunWithParent( + public void Restore_Ancestor() => RunWithParent( context => { var instance = context.CurrentInstance; - instance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); - instance.SetCurrentParallelBranchId(_parentLevel); + instance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); + instance.SetParallelBranchId(_parentLevel); + instance.GetParallelBranchId().ShouldBe(_parentLevel); }); [Fact] @@ -135,21 +141,21 @@ public void GenerateChildParallelBranchId() context => { var scheduledInstance = context.CurrentInstance; - scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); - Should.Throw(() => scheduledInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); + scheduledInstance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); + Should.Throw(() => scheduledInstance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(_parentLevel))); }); [Fact] public void ParallelBranchDoesNotLeakToSiblings() => RunWithParent( context => { - context.CurrentInstance.SetCurrentParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(context.CurrentInstance.GetCurrentParallelBranchId())); - context.CurrentInstance.GetCurrentParallelBranchId().ShouldNotBe(_parentLevel); - context.CurrentInstance.GetCurrentParallelBranchId().ShouldStartWith(_parentLevel); + context.CurrentInstance.SetParallelBranchId(ParallelTrackingExtensions.GenerateChildParallelBranchId(context.CurrentInstance.GetParallelBranchId())); + context.CurrentInstance.GetParallelBranchId().ShouldNotBe(_parentLevel); + context.CurrentInstance.GetParallelBranchId().ShouldStartWith(_parentLevel); }, context => { - var readLevel = context.CurrentInstance.GetCurrentParallelBranchId(); + var readLevel = context.CurrentInstance.GetParallelBranchId(); var branchId = context.GetCurrentParallelBranchId(); readLevel.ShouldBe(_parentLevel); branchId.ShouldBe(_parentLevel); diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs index a3751d4e..58a0cc04 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs @@ -21,7 +21,7 @@ public interface IFaultHandler void OnComplete(NativeActivityContext context, ActivityInstance completedInstance); } -public sealed class DataContractWorkflowSerializer : IWorkflowSerializer +public class DataContractWorkflowSerializer : IWorkflowSerializer { public XInstanceDictionary LoadWorkflowInstance(Stream sourceStream) => WorkflowSerializerHelpers.ToNameDictionary(GetDataContractSerializer().ReadObject(sourceStream)); @@ -29,99 +29,17 @@ public sealed class DataContractWorkflowSerializer : IWorkflowSerializer public void SaveWorkflowInstance(XInstanceDictionary workflowInstanceState, Stream destinationStream) => GetDataContractSerializer().WriteObject(destinationStream, workflowInstanceState.ToSave()); - private static DataContractSerializer GetDataContractSerializer() + protected virtual DataContractSerializer GetDataContractSerializer() { - DataContractSerializerSettings settings = new DataContractSerializerSettings + DataContractSerializerSettings settings = new() { PreserveObjectReferences = true, DataContractResolver = new WorkflowDataContractResolver() }; var dataContractSerializer = new DataContractSerializer(typeof(InstanceDictionary), settings); - dataContractSerializer.SetSerializationSurrogateProvider(new SerializationSurrogateProvider()); return dataContractSerializer; } - /// - /// WF knows how to serialize Delegates and callbacks, but *only* if they are an Activity method - /// See https://referencesource.microsoft.com/#System.Activities/System/Activities/Runtime/CallbackWrapper.cs,273 - /// Because of how global exception handling and debug tracking is implemented, we are breaking that assumption - /// We force a serialization surrogate that knows how to handle our delegates - /// We're replacing the serialization of ActivityCompletionCallbackWrapper and FaultCallbackWrapper - /// see https://github.com/Microsoft/referencesource/blob/master/System.Activities/System/Activities/Runtime/CallbackWrapper.cs - /// - [DataContract] - internal sealed class SurrogateWrapper - { - [DataMember] - public bool IsFaultCallback { get; set; } - - [DataMember] - public IFaultHandler Handler { get; set; } - - [DataMember] - public ActivityInstance ActivityInstance { get; set; } - } - - internal sealed class SerializationSurrogateProvider : ISerializationSurrogateProvider - { - private const string FaultWrapperTypeName = "System.Activities.Runtime.FaultCallbackWrapper"; - private const string CompletionWrapperTypeName = "System.Activities.Runtime.ActivityCompletionCallbackWrapper"; - - private static bool IsWrapperType(Type type) => type.FullName == FaultWrapperTypeName || type.FullName == CompletionWrapperTypeName; - public Type GetSurrogateType(Type type) => IsWrapperType(type) ? typeof(SurrogateWrapper) : null; - - public object GetObjectToSerialize(object obj, Type targetType) - { - var typeToSerialize = obj.GetType(); - System.Diagnostics.Trace.TraceInformation($"TypeToSerialize = {typeToSerialize.FullName}"); - - if (!IsWrapperType(typeToSerialize)) - { - return obj; - } - - Delegate callback = GetPrivateField(obj, "_callback"); - if (callback?.Target is not IFaultHandler handler) - { - return obj; - } - return new SurrogateWrapper - { - IsFaultCallback = callback is FaultCallback, - Handler = handler, - ActivityInstance = GetPrivateField(obj, "_activityInstance"), - }; - } - - private static T GetPrivateField(object obj, string fieldName) - { - var field = GetFieldInfo(obj.GetType(), fieldName); - var value = field?.GetValue(obj); - return value is T t ? t : default; - } - - private static FieldInfo GetFieldInfo(Type type, string fieldName) - { - return type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); - } - - public object GetDeserializedObject(object obj, Type targetType) - { - if (obj is not SurrogateWrapper surrogate) - { - return obj; - } - var originalType = typeof(Activity).Assembly.GetType(surrogate.IsFaultCallback - ? FaultWrapperTypeName - : CompletionWrapperTypeName); - - return Activator.CreateInstance(originalType, surrogate.IsFaultCallback - ? (FaultCallback)surrogate.Handler.OnFault - : (CompletionCallback)surrogate.Handler.OnComplete, - surrogate.ActivityInstance); - } - } - private sealed class WorkflowDataContractResolver : DataContractResolver { private readonly Dictionary _cachedTypes = new(); @@ -152,5 +70,4 @@ private Type FindType(string qualifiedTypeName) return type; } } - } diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs index 0125b020..e2d1bb37 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs @@ -24,7 +24,7 @@ void IWorkflowSerializer.SaveWorkflowInstance(XInstanceDictionary workflowInstan Serializer().Serialize(writer, workflowInstanceState.ToSave()); writer.Flush(); } - private static JsonSerializer Serializer() + protected virtual JsonSerializer Serializer() { var settings = SerializerSettings(); return new() diff --git a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs index 2fcd71b9..8b8808c5 100644 --- a/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs +++ b/src/UiPath.Workflow.Runtime/ParallelTrackingExtensions.cs @@ -13,29 +13,29 @@ public static class ParallelTrackingExtensions public static ActivityInstance MarkNewParallelBranch(this ActivityInstance instance) { - var parentId = instance.GetCurrentParallelBranchId(); - instance.SetCurrentParallelBranchId(GenerateChildParallelBranchId(parentId)); + var parentId = instance.GetParallelBranchId(); + instance.SetParallelBranchId(GenerateChildParallelBranchId(parentId)); return instance; } - public static string GetCurrentParallelBranchId(this ActivityInstance instance) => + public static string GetParallelBranchId(this ActivityInstance instance) => GetExecutionProperties(instance).Find(BranchIdPropertyName) as string; public static string GetCurrentParallelBranchId(this ActivityContext context) => - context.CurrentInstance?.GetCurrentParallelBranchId(); + context.CurrentInstance?.GetParallelBranchId(); /// /// Sets the parallelBranchId for the current activity instance /// /// null or empty removes the branch setting from current instance /// when not a pop or a push - public static void SetCurrentParallelBranchId(this ActivityInstance instance, string branchId) + public static void SetParallelBranchId(this ActivityInstance instance, string branchId) { - var currentBranchId = instance.GetCurrentParallelBranchId(); + var currentBranchId = instance.GetParallelBranchId(); if (!IsAncestorOf(thisStack: branchId, descendantStack: currentBranchId) && !IsAncestorOf(thisStack: currentBranchId, descendantStack: branchId)) - throw new ArgumentException($"{nameof(branchId)} must be a pop or a push.", nameof(instance)); + throw new ArgumentException($"{nameof(branchId)} must be a pop or a push.", nameof(branchId)); var props = GetExecutionProperties(instance); props.Remove(BranchIdPropertyName, skipValidations: true); From 1a9a597273e4b8c5f999c293926213cb1b90ec9b Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Wed, 15 May 2024 15:54:35 +0300 Subject: [PATCH 14/17] fix TimerTable Dispose after a load that taht decided to not set delayTimer as it is in the past see: TimerTable.OnLoad --- src/UiPath.Workflow.Runtime/Statements/TimerTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UiPath.Workflow.Runtime/Statements/TimerTable.cs b/src/UiPath.Workflow.Runtime/Statements/TimerTable.cs index 66fafe2f..92a47924 100644 --- a/src/UiPath.Workflow.Runtime/Statements/TimerTable.cs +++ b/src/UiPath.Workflow.Runtime/Statements/TimerTable.cs @@ -185,7 +185,7 @@ public void Dispose() foreach (TimerData timerData in _sortedTimerList.Timers) { //timerData.IOThreadTimer.Cancel(); - timerData.DelayTimer.Cancel(); + timerData.DelayTimer?.Cancel(); } // And we clear the table and other member variables that might cause the retry logic From 653205a77a3f60cc115fea4cccc35f7542c389e3 Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Wed, 15 May 2024 18:06:01 +0300 Subject: [PATCH 15/17] review comments 3 --- .../Persistence/AbstractInstanceStore.cs | 2 +- .../Persistence/DataContractWorkflowSerializer.cs | 7 ------- .../Persistence/JsonWorkflowSerializer.cs | 15 +-------------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs index 1829c1cb..afd8c2fb 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/AbstractInstanceStore.cs @@ -109,7 +109,7 @@ private async Task TryCommandAsync(InstancePersistenceContext context, Ins private async Task LoadWorkflow(InstancePersistenceContext context) { - var originalStream = await GetLoadStream(context.InstanceView.InstanceId); + using var originalStream = await GetLoadStream(context.InstanceView.InstanceId); var deserializedInstanceData = _instanceSerializer.LoadWorkflowInstance(originalStream); context.LoadedInstance(InstanceState.Initialized, deserializedInstanceData, null, null, null); OnLoadDone(context.InstanceView.InstanceId, originalStream); diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs index 58a0cc04..26fcf5d1 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs @@ -49,13 +49,6 @@ private sealed class WorkflowDataContractResolver : DataContractResolver public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) { - if (typeof(IEnumerable) == declaredType) //ROBO-2904 - { - typeName = null; - typeNamespace = null; - return true; - } - typeName = new XmlDictionaryString(XmlDictionary.Empty, type.AssemblyQualifiedName, 0); typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, type.Namespace, 0); diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs index e2d1bb37..51b6b7de 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/JsonWorkflowSerializer.cs @@ -24,23 +24,10 @@ void IWorkflowSerializer.SaveWorkflowInstance(XInstanceDictionary workflowInstan Serializer().Serialize(writer, workflowInstanceState.ToSave()); writer.Flush(); } - protected virtual JsonSerializer Serializer() - { - var settings = SerializerSettings(); - return new() - { - Formatting = Formatting.Indented, - TypeNameHandling = settings.TypeNameHandling, - ConstructorHandling = settings.ConstructorHandling, - ObjectCreationHandling = settings.ObjectCreationHandling, - PreserveReferencesHandling = settings.PreserveReferencesHandling, - ReferenceLoopHandling = settings.ReferenceLoopHandling - }; - } + protected virtual JsonSerializer Serializer() => JsonSerializer.Create(SerializerSettings()); public static JsonSerializerSettings SerializerSettings() => new() { - Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Auto, ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, ObjectCreationHandling = ObjectCreationHandling.Replace, From 6d7960a1a0b673dc156178ce606a39b7c1a87cec Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Thu, 16 May 2024 21:08:51 +0300 Subject: [PATCH 16/17] review comments 4 --- src/Test/TestCases.Activities/Flowchart/Execution.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/TestCases.Activities/Flowchart/Execution.cs b/src/Test/TestCases.Activities/Flowchart/Execution.cs index 2acb2943..8b8ea799 100644 --- a/src/Test/TestCases.Activities/Flowchart/Execution.cs +++ b/src/Test/TestCases.Activities/Flowchart/Execution.cs @@ -473,8 +473,8 @@ public void FiveLevelDeepEmptyNestedFlowchart() /// Five level deep nested flowchart with blocking activity /// /// Disabled and failed in desktop - //[Fact] - private void FiveLevelDeepNestedFlowchartWithBlockingActivity() + [Fact] + public void FiveLevelDeepNestedFlowchartWithBlockingActivity() { TestFlowchart parent = new TestFlowchart(); TestFlowchart child1 = new TestFlowchart(); From 1a5d1692527d18699ffa3df2bbc00863c16c96dc Mon Sep 17 00:00:00 2001 From: Mihai Radu Date: Fri, 17 May 2024 09:26:42 +0300 Subject: [PATCH 17/17] review comments 5 --- .../Persistence/DataContractWorkflowSerializer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs index 26fcf5d1..c15bec60 100644 --- a/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs +++ b/src/Test/WorkflowApplicationTestExtensions/Persistence/DataContractWorkflowSerializer.cs @@ -49,6 +49,13 @@ private sealed class WorkflowDataContractResolver : DataContractResolver public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) { + if (typeof(IEnumerable) == declaredType) + { + typeName = null; + typeNamespace = null; + return true; + } + typeName = new XmlDictionaryString(XmlDictionary.Empty, type.AssemblyQualifiedName, 0); typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, type.Namespace, 0);