Skip to content

Using Saga, UserTask, and Jumps results in two user options after exception #266

@fubar-coder

Description

@fubar-coder

After the first exception (see example code), the GetOpenUserActions function returns two user actions: start, and step1. Is it a misuse on my side or do "jumps" not work with saga transactions? The jumps were mentioned in issue #167.

Example code:

using System;
using System.Collections.Generic;
using WorkflowCore.Interface;
using WorkflowCore.Models;
using Xunit;
using FluentAssertions;
using System.Linq;
using System.Threading.Tasks;
using WorkflowCore.Testing;
using WorkflowCore.Users.Models;

namespace WorkflowCore.IntegrationTests.Scenarios
{
    public class RetrySagaWithThreeUserTaskScenario : WorkflowTest<RetrySagaWithThreeUserTaskScenario.Workflow, RetrySagaWithThreeUserTaskScenario.MyDataClass>
    {
        private static readonly ExpectedValues[] _expectedValues = new[]
        {
            new ExpectedValues("start", "start", "Step 1", "step1"),
            new ExpectedValues("step1", "step1", "Step 2", "step2"),
            new ExpectedValues("step1", "step1", "Step 2", "step2"),
            new ExpectedValues("step1", "step1", "Step 2", "step2"),
            new ExpectedValues("step2", "step2", "Step 3", "step3"),
        };

        public class MyDataClass
        {
        }

        public class Workflow : IWorkflow<MyDataClass>
        {
            public static int Event1Fired;
            public static int Event2Fired;
            public static int Event2Passed;
            public static int Event3Fired;
            public static int TailEventFired;
            public static int StartCompensation1Fired;
            public static int StartCompensation2Fired;
            public static int StartCompensation3Fired;
            public static int Compensation1Fired;
            public static int Compensation2Fired;
            public static int Compensation3Fired;

            public string Id => "RetrySagaWithThreeUserTasksWorkflow";
            public int Version => 1;
            public void Build(IWorkflowBuilder<MyDataClass> builder)
            {
                var start = builder
                    .StartWith(_ => ExecutionResult.Next());
                var step1 = builder
                    .StartWith(_ => ExecutionResult.Next());
                var step2 = builder
                    .StartWith(_ => ExecutionResult.Next());
                var step3 = builder
                    .StartWith(_ => ExecutionResult.Next());

                start
                    .CompensateWith(_ => StartCompensation1Fired++)
                    .Saga(x => x
                        .StartWith(_ => ExecutionResult.Next())
                        .UserTask("start", _ => "start")
                        .WithOption("step1", "Step 1")
                        .Do(wb => wb
                            .StartWith(_ => ExecutionResult.Next())
                            .Then(_ => Event1Fired++)
                            .Then(step1))
                        .CompensateWith(_ => Compensation1Fired++))
                    .OnError(WorkflowErrorHandling.Retry, TimeSpan.Zero);

                step1
                    .CompensateWith(_ => StartCompensation2Fired++)
                    .Saga(x => x
                        .StartWith(_ => ExecutionResult.Next())
                        .UserTask("step1", _ => "step1")
                        .WithOption("step2", "Step 2")
                        .Do(wb => wb
                            .StartWith(_ => ExecutionResult.Next())
                            .Then(_ =>
                            {
                                Event2Fired++;
                                if (Event2Fired < 3)
                                    throw new Exception();
                                Event2Passed++;
                            })
                            .Then(step2))
                        .CompensateWith(_ => Compensation2Fired++))
                    .OnError(WorkflowErrorHandling.Retry, TimeSpan.Zero);

                step2
                    .CompensateWith(_ => StartCompensation3Fired++)
                    .Saga(x => x
                        .StartWith(_ => ExecutionResult.Next())
                        .UserTask("step2", _ => "step2")
                        .WithOption("step3", "Step 3")
                        .Do(wb => wb
                            .StartWith(_ => ExecutionResult.Next())
                            .Then(_ => Event3Fired++)
                            .Then(step3))
                        .CompensateWith(_ => Compensation3Fired++))
                    .OnError(WorkflowErrorHandling.Retry, TimeSpan.Zero);

                step3
                    .Then(_ => TailEventFired++);
            }
        }

        public RetrySagaWithThreeUserTaskScenario()
        {
            Setup();
            Workflow.Event1Fired = 0;
            Workflow.Event2Fired = 0;
            Workflow.Event2Passed = 0;
            Workflow.Event3Fired = 0;
            Workflow.StartCompensation1Fired = 0;
            Workflow.StartCompensation2Fired = 0;
            Workflow.StartCompensation3Fired = 0;
            Workflow.Compensation1Fired = 0;
            Workflow.Compensation2Fired = 0;
            Workflow.Compensation3Fired = 0;
            Workflow.TailEventFired = 0;
        }

        [Fact]
        public async Task Scenario()
        {
            var workflowId = StartWorkflow(new MyDataClass());
            var instance = await Host.PersistenceStore.GetWorkflowInstance(workflowId);

            string oldUserOptionKey = null;
            foreach (var expectedValue in _expectedValues)
            {
                var userOptions = await WaitForDifferentUserStepAsync(instance, TimeSpan.FromSeconds(1), oldUserOptionKey);
                userOptions.Count.Should().Be(1);

                var userOption = userOptions.Single();
                userOption.Prompt.Should().Be(expectedValue.Prompt);
                userOption.AssignedPrincipal.Should().Be(expectedValue.AssignedPrincipal);
                userOption.Options.Count.Should().Be(1);

                var selectionOption = userOption.Options.Single();
                selectionOption.Key.Should().Be(expectedValue.OptionKey);
                selectionOption.Value.Should().Be(expectedValue.OptionValue);
                await Host.PublishUserAction(userOption.Key, string.Empty, selectionOption.Value);

                oldUserOptionKey = userOption.Key;
            }

            WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30));

            GetStatus(workflowId).Should().Be(WorkflowStatus.Complete);
            UnhandledStepErrors.Count.Should().Be(2);
            Workflow.Event1Fired.Should().Be(1);
            Workflow.Event2Fired.Should().Be(3);
            Workflow.Event2Passed.Should().Be(1);
            Workflow.Event3Fired.Should().Be(1);
            Workflow.StartCompensation1Fired.Should().Be(0);
            Workflow.StartCompensation2Fired.Should().Be(0);
            Workflow.StartCompensation3Fired.Should().Be(0);
            Workflow.Compensation1Fired.Should().Be(0);
            Workflow.Compensation2Fired.Should().Be(2);
            Workflow.Compensation3Fired.Should().Be(0);
            Workflow.TailEventFired.Should().Be(1);
        }

        private static async Task<IReadOnlyCollection<OpenUserAction>> WaitForDifferentUserStepAsync(
            WorkflowInstance instance,
            TimeSpan timeout,
            string oldUserActionKey = null)
        {
            var startTime = DateTime.UtcNow;

            while (DateTime.UtcNow - startTime <= timeout)
            {
                var userActions = await WaitForUserStepAsync(instance);

                if (oldUserActionKey != null && userActions.Any(x => x.Key == oldUserActionKey))
                {
                    continue;
                }

                return userActions;
            }

            return Array.Empty<OpenUserAction>();
        }

        private static async Task<IReadOnlyCollection<OpenUserAction>> WaitForUserStepAsync(WorkflowInstance instance)
        {
            var delayCount = 200;
            var openActions = instance.GetOpenUserActions()?.ToList();
            while ((openActions?.Count ?? 0) == 0)
            {
                await Task.Delay(TimeSpan.FromMilliseconds(10));
                openActions = instance.GetOpenUserActions()?.ToList();
                if (delayCount-- == 0)
                {
                    break;
                }
            }

            return openActions;
        }

        private class ExpectedValues
        {
            public ExpectedValues(string prompt, string assignedPrincipal, string optionKey, string optionValue)
            {
                Prompt = prompt;
                AssignedPrincipal = assignedPrincipal;
                OptionKey = optionKey;
                OptionValue = optionValue;
            }

            public string Prompt { get; }
            public string AssignedPrincipal { get; }
            public string OptionKey { get; }
            public string OptionValue { get; }
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions