question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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

Issue Analytics

  • State:open
  • Created 5 years ago
  • Comments:9 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
danielgerlagcommented, Feb 24, 2019

v1.8.3

0reactions
fubar-codercommented, Apr 4, 2019

Not exactly sure what you mean, but this is what I wanted to achieve:

  • Have a workflow with multiple user tasks (read: vertices)
  • Have one or more options (with attached actions) (read: edges)
  • Select an option for the currently active user task
    • Successfull execution of action: Move on to the next user task (vertex)
    • At least on action failed (non-recoverable):
      • Revert all actions for the option (optional)
      • Jump to the user task where we chose the option from

In other words: It can be seen as a graph with vertices, edges and actions attached to the edge to be executed during the transition from vertex A to B with error recovery.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is there a way to go back to any previous step? · Issue #167
Is there a way to define the next step using the fluent api, ... Using Saga, UserTask, and Jumps results in two user...
Read more >
Investigating the Link Between Users' IT Adaptation
Behaviours and Individual-Level IT Use Outcomes Using the. Coping Model of User Adaptation: A Case Study of a Work. System Computerisation Project.
Read more >
Using axios in Vue.js with separate service?
You can return the promise from the request module and use it anywhere. For example, sendGet(url) { const token = localStorage.
Read more >
Untitled
Egg in a hole with cheese and bacon, Reformation bodysuit avalon, ... Madeira spain travel, Results from halle tennis, Paint 12x12 room, Rancore...
Read more >
Lecture Notes in Artificial Intelligence 2702
Methods for personalizing human-computer interaction based on user models have been successfully developed and applied in a number of domains, such as ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found