New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature - Workflow Middleware #684
Feature - Workflow Middleware #684
Conversation
A few things that I still need to do:
|
Re: Exception handling in post-workflow middleware. Here's another idea that's an offshoot of your suggestion of introducing an What if we add another method to Here's an example of how that could look: public class MyWorkflow: IWorkflow<object> {
public string Id => nameof(MyWorkflow);
public int Version => 1;
public void Build(IWorkflowBuilder<object> builder) =>
builder
.OnPostMiddlewareException<MyPostMiddlewareExceptionHandler>()
.StartWith<SomeStep>();
}
public class MyPostMiddlewareExceptionHandler : IPostWorkflowMiddlewareExceptionHandler {
public MyPostMiddlewareExceptionHandler(...) {
// Will fetch from DI so you can inject whatever dependencies you
// need in case you want to ship the error to DB or some external service
}
public Task Handle(Exception ex) {
// Handle it somehow here
}
} |
How would that work for JSON or YAML defined workflows? |
Oh, I hadn't considered that... Could it work the same way that you specify Id: AddWorkflow
Version: 1
DataType: MyApp.MyDataClass, MyApp
OnPostMiddlewareException: MyApp.MyPostMiddlewareExceptionHandler, MyApp
Steps:
- Id: Hello
StepType: MyApp.HelloWorld, MyApp
NextStepId: Add
- Id: Add
StepType: MyApp.AddNumbers, MyApp
NextStepId: Bye
Inputs:
Value1: data.Value1
Value2: data.Value2
Outputs:
Answer: step.Result
- Id: Bye
StepType: MyApp.GoodbyeWorld, MyApp |
/// <summary> | ||
/// Determines at which point to run the middleware. | ||
/// </summary> | ||
public enum WorkflowMiddlewarePhase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had considered also having a WorkflowMiddlewarePhase.Both
to enable middleware that runs both pre/post. Do you think that would be a good idea?
MaxRetries | ||
); | ||
|
||
// TODO: Come up with way to persist workflow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a todo here. We should figure out if it makes sense to allow you to persist workflow steps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, re: persistence of steps. In our app that uses Workflow Core, we already have a way of persisting steps so we have a work around for now. In the interest of getting this PR out sooner, I'm thinking of not tackling step persistence as part of this PR and tackling it as a separate issue. Sound good?
|
||
[Test] | ||
public async void AcquiresLock() | ||
public async Task AcquiresLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I was running the tests, NUnit was complaining that these tests were invalid because they were async void
instead of async Task
. When you have async void
, it does not give NUnit the hooks it needs to wait for the test to complete.
Seems like its having trouble running some of the integration tests in appveyor and I'm not sure why. I tried to follow the same pattern as all of the other integration tests. Is there any reason you can see that these would stall? This is the offending test: Edit: I have a feeling that it is due to threading mixing with async tasks. I've created async overloads of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic contribution! Thank you so much!
@@ -83,6 +85,8 @@ public async Task<string> StartWorkflow<TData>(string workflowId, int? version, | |||
|
|||
wf.ExecutionPointers.Add(_pointerFactory.BuildGenesisPointer(def)); | |||
|
|||
await _middlewareRunner.RunPreMiddleware(wf); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think there would be use cases where the pre-workflow middleware would want access to the ID of the instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ID of the WorkflowInstance
? Yes. And with the way that the interface looks, they should have access to every property of the WorkflowInstance
since it gets passed down to the Handle
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but the ID is generated when you first persist the workflow, which hasn't happened at this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it is? Is the ID always a Guid? If so, we can rely on that. I was hoping to have it run before the persistence step so that any changes to the workflow (i.e. setting the description) would be persisted along with it.
Would it make sense to persist it once before and once after?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not strictly a Guid... it's up to the implementation of the persistence provider...
If we are going to persist it and then do more work... we'd also probably need to hold a lock on the workflow ID, so that none of the workers pick it up and try to process it before we've finished. There is potential for a race condition here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... that would add a bit of complexity to it. So how about we just document that the workflow does not have an id in the pre-workflow middleware and keep it prior to the initial persistence?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we can revisit this in future versions
/// <summary> | ||
/// Runner with the ability to apply middleware to steps and workflows. | ||
/// </summary> | ||
public class WorkflowMiddlewareRunner : IWorkflowMiddlewareRunner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think this class should also execute the actual step?
If we go that route... should we maybe name it as to indicate that it also has this responsibility? (but now it'd have 2 responsibilities)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it stands now the workflow middleware runner does execute the step as the last "next
" in the step middleware chain. This is deliberate to allow step middleware to add logic around the execution of a step and even potentially change the step's result.
Are you suggesting to not have it run the step?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think we it might help if the name of the class indicated that it also ran the step?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah gotcha. Sounds good. Will rename to reflect that. Something like WorkflowMiddlewareAndStepRunner
? Or perhaps WorkflowStepRunner
and the middleware portion is implied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... Thinking about this some more. WorkflowMiddlewareAndStepRunner
doesn't exactly sound well. Then I tried changing it to WorkflowStepRunner
but that doesn't imply that it also runs pre/post workflow middleware. So regarding naming, I think we have these options:
- Keep it as
WorkflowMiddlewareRunner
and document that it also runs the step in the method name and comments. We could always renameRunStep
->RunStepWithMiddleware
to emphasize that it runs the step and also runs middleware around it. - Split the class into two classes, one for running pre/post middleware and the other for running steps with middleware. I'd probably call them
WorkflowMiddlewareRunner
andWorkflowStepRunner
respectively. Although the major con here is that there will be two dependencies needed to be added to the executor constructor instead of one.
I'm in favor of option 1. What are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the main objective is to run the step, and running the middleware is a secondary part of that, so in that case I would opt to name it something like StepExecutor
and keep the functionality as you have it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I like StepExecutor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this being called StepExecutor
now, I think it makes even more sense to split it into two classes, one for steps and one for workflow pre/post middleware. Do you agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at my latest changes, I split it out and it made testing much easier and follows the single responsibility principle quite nicely. Now there's StepExecutor
which executes the step with middleware and WorkflowMiddlewareRunner
ONLY runs pre/post workflow. This felt the most natural.
|
||
// Add some pre workflow middleware | ||
// This middleware will run before the workflow starts | ||
services.AddWorkflowMiddleware<AddDescriptionWorkflowMiddleware>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we have some mechanism for defining the priority / order the middleware executes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about that as well, but it would introduce some additional complexity and also probably add some additional fields to the interface. In theory, I could have some kind of field like the following on the interface:
int? Order { get; }
However, then every consumer would be forced to implement it even in cases where they don't care about the order. Do you feel the additional complexity is warranted? Are there any use-cases where just changing the order in which the middleware are defined is not sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we leave it as a possibility for future versions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sounds good. So leave it as is for now?
Checked in the new exception handling for post workflow middleware. Take a look and tell me what you think. I also retrofitted the code to be able to load it from yaml as well. Updated the sample 19 with example usage as well. |
Docs fresh off the press! Let me know what you think: |
@@ -18,14 +18,16 @@ public class WorkflowDefinition | |||
|
|||
public WorkflowErrorHandling DefaultErrorBehavior { get; set; } | |||
|
|||
public TimeSpan? DefaultErrorRetryInterval { get; set; } | |||
public Type OnPostMiddlewareError { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to define how middleware fails on the workflow level or the global level?
What were the uses cases you had in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, as it stands now, you can do either. You can define it at the global level by implementing your own IWorkflowMiddlewareErrorHandler
or overriding it on the individual workflow level by using OnPostMiddlewareError
.
One potential use case I can think of is if your workflow middleware does something like shipping workflow metrics to a timeseries DB like InfluxDB. If for whatever reason the connection to influx is down, you may want to add some special handling for the workflows like queueing up the metrics to ship later on when it comes back up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just wondering if it makes sense to be able to define it on the individual workflow level? What uses cases does that enable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good point. None off the top of my head that couldn't be handled with a global handler. Should I remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think so... if we find use cases for it we can always add it but let's try manage the complexity for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. Removed it and updated docs, examples, and tests.
…round workflow steps and before/after workflow Add sample with a sample middleware for retrying and log correlation as well as workflow pre/post samples Add async overloads for StartWorkflow and WaitForWorkflowToComplete in integration tests Add error handling of post workflow middleware Add docs for workflow middleware
@dflor003 The middleware scenario test is failing on my local machine for some reason. |
@danielgerlag Hey! Just saw this. Hmm... This is the integration test? I was seeing that some of the tests will sporadically fail to wait for the workflow to complete. After some re-runs, it usually passes. I have a feeling that it may be due to threading and async mixing together which is why I introduced |
Oh damn... Wait. I think there's a race condition somewhere. Never mind. It may be unrelated to that. It's an issue with the test itself. |
I'll have a PR out shortly fixing it. Just need to await a few things. |
@danielgerlag Take a look at #690. That should fix the issue. |
PR can close #678, #665, and possibly a few other issues related to retry policies around steps.