Additional API surface area for Dapr Workflow authoring SDK#1012
Conversation
Signed-off-by: Chris Gillum <cgillum@microsoft.com>
Codecov Report
@@ Coverage Diff @@
## master #1012 +/- ##
==========================================
+ Coverage 69.90% 69.92% +0.01%
==========================================
Files 157 157
Lines 5227 5227
Branches 562 562
==========================================
+ Hits 3654 3655 +1
+ Misses 1440 1439 -1
Partials 133 133
Flags with carried forward coverage won't be shown. Click here to find out more.
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
| /// </para> | ||
| /// <para> | ||
| /// Workflows may be replayed multiple times to rebuild their local state after being reloaded into memory. | ||
| /// workflow code must therefore be <em>deterministic</em> to ensure no unexpected side effects from execution |
There was a problem hiding this comment.
I think I remember a comment about workflows must be thought as idempotent -- while the activities themselves don't need to be. Not sure if this idempotency is a concept we want to highlight here
There was a problem hiding this comment.
Actually, workflows don't need to be idempotent because they don't have any external side effects, which essentially guarantees effectively-once execution guarantees. The only restriction therefore is that workflows are deterministic.
Activities don't need to be deterministic, but it is a good practice to make them idempotent since they have at-least-once execution guarantees. I believe we discuss this in the comments for the WorkflowActivity class.
| /// Workflow code is tightly coupled with its execution history so special care must be taken when making changes | ||
| /// to workflow code. For example, adding or removing activity tasks to a workflow's code may cause a | ||
| /// mismatch between code and history for in-flight workflows. To avoid potential issues related to workflow | ||
| /// versioning, consider applying the following code update strategies: |
| if (this.workflowState == null) | ||
| { | ||
| return WorkflowRuntimeStatus.Unknown; | ||
| } | ||
|
|
||
| switch (this.workflowState.RuntimeStatus) | ||
| { | ||
| case OrchestrationRuntimeStatus.Running: | ||
| return WorkflowRuntimeStatus.Running; | ||
| case OrchestrationRuntimeStatus.Completed: | ||
| return WorkflowRuntimeStatus.Completed; | ||
| case OrchestrationRuntimeStatus.Failed: | ||
| return WorkflowRuntimeStatus.Failed; | ||
| case OrchestrationRuntimeStatus.Terminated: | ||
| return WorkflowRuntimeStatus.Terminated; | ||
| case OrchestrationRuntimeStatus.Pending: | ||
| return WorkflowRuntimeStatus.Pending; | ||
| default: | ||
| return WorkflowRuntimeStatus.Unknown; | ||
| } |
There was a problem hiding this comment.
So the only thing this is doing is:
- null => Unknown
- Suspended => Unknown
- everything else is returned as is. Is that it?
I am trying to understand if this couldn't be replaced with return this.workflowState?.RuntimeStatus ?? WorkflowRuntimeStatus.Unknown and the Suspended state is the only thing that would break this, so I am trying to understand Suspended was omitted from that switch on purpose or by accident.
There was a problem hiding this comment.
Ok, just noticed that this is doing a type mapping from OrchestrationRuntimeStatus to WorkflowRuntimeStatus. Anyway, re-checking if Suspended => should be equivalent to Unknown.
There was a problem hiding this comment.
Yes, so I unfortunately the 1.0.0-rc.1 version of the Durable Task SDK doesn't have the Suspended enum yet, so I can't make that change until the 1.0.0 version gets published. So for now, WorkflowRuntimeStatus.Suspended is unused until probably the next release.
| /// </para> | ||
| /// <para> | ||
| /// Workflows may be replayed multiple times to rebuild their local state after being reloaded into memory. | ||
| /// workflow code must therefore be <em>deterministic</em> to ensure no unexpected side effects from execution |
There was a problem hiding this comment.
Actually, workflows don't need to be idempotent because they don't have any external side effects, which essentially guarantees effectively-once execution guarantees. The only restriction therefore is that workflows are deterministic.
Activities don't need to be deterministic, but it is a good practice to make them idempotent since they have at-least-once execution guarantees. I believe we discuss this in the comments for the WorkflowActivity class.
| if (this.workflowState == null) | ||
| { | ||
| return WorkflowRuntimeStatus.Unknown; | ||
| } | ||
|
|
||
| switch (this.workflowState.RuntimeStatus) | ||
| { | ||
| case OrchestrationRuntimeStatus.Running: | ||
| return WorkflowRuntimeStatus.Running; | ||
| case OrchestrationRuntimeStatus.Completed: | ||
| return WorkflowRuntimeStatus.Completed; | ||
| case OrchestrationRuntimeStatus.Failed: | ||
| return WorkflowRuntimeStatus.Failed; | ||
| case OrchestrationRuntimeStatus.Terminated: | ||
| return WorkflowRuntimeStatus.Terminated; | ||
| case OrchestrationRuntimeStatus.Pending: | ||
| return WorkflowRuntimeStatus.Pending; | ||
| default: | ||
| return WorkflowRuntimeStatus.Unknown; | ||
| } |
There was a problem hiding this comment.
Yes, so I unfortunately the 1.0.0-rc.1 version of the Durable Task SDK doesn't have the Suspended enum yet, so I can't make that change until the 1.0.0 version gets published. So for now, WorkflowRuntimeStatus.Suspended is unused until probably the next release.
halspang
left a comment
There was a problem hiding this comment.
Overall this looks good pretty good to me. I just have a few questions but I don't see anything glaring in here.
| /// <param name="context">The workflow's context.</param> | ||
| /// <param name="input">The workflow's input.</param> | ||
| /// <returns>Returns the workflow output as the result of a <see cref="Task"/>.</returns> | ||
| Task<object?> RunAsync(WorkflowContext context, object? input); |
There was a problem hiding this comment.
For my own edification, the reason we're doing it this way is so that when the customer extends the abstract class, they can still specify the type variables, right?
There was a problem hiding this comment.
The main reason we're doing this is so that we can have common code that can run the user's code without needing to worry about the specific type parameters. I don't recall exactly, but I think there wasn't a way for me to write this common code when generics were involved. It was a problem I originally encountered in the design of the Durable Task SDK that we depend on.
| /// Users should not implement workflows using this interface, directly. | ||
| /// Instead, <see cref="Workflow{TInput, TOutput}"/> should be used to implement workflows. | ||
| /// </remarks> | ||
| public interface IWorkflow |
There was a problem hiding this comment.
If we don't want users to use this, why not make it internal or just provide the abstract class?
There was a problem hiding this comment.
I can't make it internal because there are public APIs that depend on it (like the RegisterWorkflow<T>() API). I believe there was some reason I needed this because of constraints related to how generics work in .NET, though I can't remember exactly what it was...
| /// Users should not implement workflow activities using this interface, directly. | ||
| /// Instead, <see cref="WorkflowActivity{TInput, TOutput}"/> should be used to implement workflow activities. | ||
| /// </remarks> | ||
| public interface IWorkflowActivity |
There was a problem hiding this comment.
Same questions from Workflow here.
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method is primarily designed for eternal workflows, which are workflows that |
There was a problem hiding this comment.
Are eternal workflows common? The idea of that scares me 😅 I'm fine keeping this, just curious.
There was a problem hiding this comment.
Yes, it's actually very common. 😅 It's a way for you to create some kind of long running agent that doesn't have an explicit lifetime, like a monitor that maybe runs in a loop and only takes action when some event occurs.
Signed-off-by: Chris Gillum <cgillum@microsoft.com>
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method is primarily designed for eternal workflows, which are workflows that |
There was a problem hiding this comment.
Yes, it's actually very common. 😅 It's a way for you to create some kind of long running agent that doesn't have an explicit lifetime, like a monitor that maybe runs in a loop and only takes action when some event occurs.
| /// Users should not implement workflows using this interface, directly. | ||
| /// Instead, <see cref="Workflow{TInput, TOutput}"/> should be used to implement workflows. | ||
| /// </remarks> | ||
| public interface IWorkflow |
There was a problem hiding this comment.
I can't make it internal because there are public APIs that depend on it (like the RegisterWorkflow<T>() API). I believe there was some reason I needed this because of constraints related to how generics work in .NET, though I can't remember exactly what it was...
| /// <param name="context">The workflow's context.</param> | ||
| /// <param name="input">The workflow's input.</param> | ||
| /// <returns>Returns the workflow output as the result of a <see cref="Task"/>.</returns> | ||
| Task<object?> RunAsync(WorkflowContext context, object? input); |
There was a problem hiding this comment.
The main reason we're doing this is so that we can have common code that can run the user's code without needing to worry about the specific type parameters. I don't recall exactly, but I think there wasn't a way for me to write this common code when generics were involved. It was a problem I originally encountered in the design of the Durable Task SDK that we depend on.
Signed-off-by: Chris Gillum <cgillum@microsoft.com>
halspang
left a comment
There was a problem hiding this comment.
One small comment and can I also ask you to update the .csproj for workflows to no longer target net5? We're removing it in another PR so if you don't want to I can get it there as well.
|
|
||
| serviceCollection.AddOptions<WorkflowRuntimeOptions>().Configure(configure); | ||
|
|
||
| static bool TryGetGrpcAddress(out string address) |
There was a problem hiding this comment.
We have a shared package that may already do what you need here.
https://github.com/dapr/dotnet-sdk/blob/master/src/Shared/DaprDefaults.cs
There was a problem hiding this comment.
Thanks for pointing this out, I hadn't seen it. I ran into a couple problems trying to use it as-is though. For now, I'm going to put a TODO comment in the code suggesting that we adopt the common code if/when the problems can be safely addressed.
Signed-off-by: Chris Gillum <cgillum@microsoft.com>
Description
This PR adds a significant amount of API surface area to the workflow authoring SDK. It's a follow-up to #981, which had some feedback comments still unresolved. The other motivation for this work was that building the demo highlighted some significant gaps in the development experience.
Summary of changes:
WorkfloworWorkflowActivitybase classes respectively.WorkflowContextobject, allowing workflows to be far more expressive (matching what the Durable Task SDK supports).WorkflowClienttoWorkflowEngineClientto make it more distinct from the Dapr client and added several more APIs to hide the internal dependencies.WorkflowMetadatatoWorkflowState(warning, Git made some incorrect assumptions about which files were renamed)NOTE: The careful reader will notice that a lot of the code in this SDK is a very thin wrapper around the Durable Task SDK. I do wonder if trying to hide the Durable Task SDK from the Dapr SDK will at some point become unsustainable and confusing for developers. I worry that we won't be able to fully hide the fact that the Dapr Workflow engine is implemented using the Durable Task Framework. An alternative approach to what I'm doing now would be to tell Dapr developers to simply use the Durable Task SDK directly, and we could ship some Dapr-specific extension methods that work with it. This would be far less confusing and less burdensome to Dapr maintainers since the amount of code they need to maintain is dramatically reduced. However, I'm not sure how the Dapr maintainers/TOC would feel about this from a branding/control perspective. I'm open to opinions, though we don't necessarily need to block this PR on that discussion.
Issue reference
This PR is related to the following GitHub issues:
Checklist
Please make sure you've completed the relevant tasks for this PR, out of the following list: