[Workflows] Introduce `Raise Error` capability (to complement `Raise Event`)
In what area(s)?
/area runtime
Describe the feature
Background
In most languages, errors and exceptions are used to gracefully handle unexpected conditions, and/or for controlling flow.
Dapr Workflows embraces Exception handling with Activities -- If an Activity throws an exception / error, it is marshalled back to the Workflow code, where it can be caught in the Workflow code. Great!
Sadly the capability does not exist when using the Raise Event capability, there is no behavioural equivalent of throwing an Exception / error here, which leads to ugly boiler plate code and inefficient workarounds.
Example 1
In the below example, its trivial to handle the unhappy path of Activities, with Exception handling.
public override async Task<bool> RunAsync(WorkflowContext context, object state)
{
try
{
var all = new List<Task<bool>>
{
context.CallActivityAsync<bool>("foo"),
context.CallActivityAsync<bool>("bar"),
};
await Task.WhenAll(all);
// happy path processing begins here... yay
}
catch (FooException ex)
{
// unhappy path
}
catch (BarException ex)
{
// unhappy path
}
} Example 2 - Include a WaitForExternalEvent
Now let's extend on Example 1 and include a WaitForExternalEvent into the list of tasks we want to wait for.
Sadly the pattern breaks down, we can't simply catch a related Exception / error here, as there is no API to do so.
public override async Task<bool> RunAsync(WorkflowContext context, object state)
{
try
{
var all = new List<Task<bool>>
{
context.CallActivityAsync<bool>("foo"),
context.CallActivityAsync<bool>("bar"),
context.WaitForExternalEventAsync<bool>("foobar_event")
};
await Task.WhenAll(all);
// happy path processing begins here... yay
}
catch (FooException ex)
{
// unhappy path
}
catch (BarException ex)
{
// unhappy path
}
catch (FooBarException ex)
{
// THIS IS NOT POSSIBLE
}
} Example 3 - Work-around
- Added more boilerplate to separate out the tasks results and event results.
- Added a new
eventcalledfoobar_errorto simulate a first-class error / Exception. - Manually convert the
foobar_errorinto a first-class error / Exception - As we are now outside of the
WhenAny/WhenAllwe have no way to react to the other Tasks completing. So we have to introduce a horrificwhileloop to manually check for the happy path scenario, which is inefficient from a workflow history perspective, and is a subjectively bad dev ex.
public override async Task<bool> RunAsync(WorkflowContext context, object state)
{
try
{
var tasks = new List<Task<bool>>();
var foo = context.CallActivityAsync<bool>("foo");
var bar = context.CallActivityAsync<bool>("bar");
tasks.Add(foo);
tasks.Add(bar);
var events = new List<Task<bool>>();
var error = context.WaitForExternalEventAsync<bool>("foobar_error");
var foobar = context.WaitForExternalEventAsync<bool>("foobar_event");
events.Add(foobar);
events.Add(error);
var anyEvent = Task.WhenAny(events);
var allTasks = Task.WhenAll(tasks);
var winner = await Task.WhenAny(anyEvent, allTasks);
if (error == winner)
{
// trigger unhappy path
throw new FooBarException();
}
// at this point we've no idea what tasks have completed yet so we need to go into a manual loop which isn't nice at all
while (!allTasks.IsCompleted && !foobar.IsCompleted)
{
await context.CreateTimer(TimeSpan.FromSeconds(5)); //<---- this sucks
}
// happy path processing begins here... yay
}
catch (FooException ex)
{
// unhappy path
}
catch (BarException ex)
{
// unhappy path
}
catch (FooBarException ex)
{
// unhappy path
}
} Proposal
I believe we need to find a solution to deliver the experience akin to Example 2
I propose that we introduce a complementary first-class Raise Error capability on the Workflow API.
This would allow a caller to utilise Raise Event for the happy path, and Raise Error for the Unhappy path, offering both control flow and exception handling, creating parity with Activities.
// workflow
var foobar = context.WaitForExternalEventAsync<bool>("foobar_event");
// external system
if (happy)
workflowClient.RaiseEventAsync("foobar_event", true);
else
workflowClient.RaiseErrorAsync("This is a custom error message description", ex) // ex is of Type `FooBarException`Release Note
RELEASE NOTE:
Source: dapr/dapr