Using Saga, UserTask, and Jumps results in two user options after exception
See original GitHub issueAfter 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:
- Created 5 years ago
- Comments:9 (6 by maintainers)
Top 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 >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found

v1.8.3
Not exactly sure what you mean, but this is what I wanted to achieve:
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.