-
Notifications
You must be signed in to change notification settings - Fork 374
feat(workflow): align history propagation API with go-sdk #1825
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
Changes from 5 commits
d1e40fe
b81c4ea
fcb1993
34a2c98
57c69da
b604220
36c2788
25fe1d7
6909ac2
da2f380
5987d9c
166b288
a5a24cd
ef9e588
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // ------------------------------------------------------------------------ | ||
| // Copyright 2026 The Dapr Authors | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| // ------------------------------------------------------------------------ | ||
|
|
||
| namespace Dapr.Workflow; | ||
|
|
||
| /// <summary> | ||
| /// A reconstructed view of a single activity invocation from propagated history. | ||
| /// </summary> | ||
| /// <param name="Name">The scheduled name of the activity.</param> | ||
| /// <param name="Started">Whether the activity was scheduled in the propagated chunk.</param> | ||
| /// <param name="Completed">Whether the activity completed successfully.</param> | ||
| /// <param name="Failed">Whether the activity failed.</param> | ||
| /// <param name="Input">The JSON-encoded input payload, or <c>null</c> when unset.</param> | ||
| /// <param name="Output">The JSON-encoded output payload, or <c>null</c> when the activity has not completed.</param> | ||
| /// <param name="FailureDetails">The failure details when <paramref name="Failed"/> is true, otherwise <c>null</c>.</param> | ||
| /// <remarks> | ||
| /// Mirrors the <c>ActivityResult</c> struct in the Go SDK and the | ||
| /// <c>ActivityResult</c> dataclass in the Python SDK so cross-language | ||
| /// quickstarts and chain-of-custody patterns line up. | ||
| /// </remarks> | ||
| public sealed record ActivityResult( | ||
| string Name, | ||
| bool Started, | ||
| bool Completed, | ||
| bool Failed, | ||
| string? Input, | ||
| string? Output, | ||
| WorkflowTaskFailureDetails? FailureDetails); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // ------------------------------------------------------------------------ | ||
| // Copyright 2026 The Dapr Authors | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| // ------------------------------------------------------------------------ | ||
|
|
||
| namespace Dapr.Workflow; | ||
|
|
||
| /// <summary> | ||
| /// A reconstructed view of a single child workflow invocation from propagated history. | ||
| /// </summary> | ||
| /// <param name="Name">The scheduled name of the child workflow.</param> | ||
| /// <param name="Started">Whether the child workflow was scheduled in the propagated chunk.</param> | ||
| /// <param name="Completed">Whether the child workflow completed successfully.</param> | ||
| /// <param name="Failed">Whether the child workflow failed.</param> | ||
| /// <param name="Output">The JSON-encoded output payload, or <c>null</c> when the child workflow has not completed.</param> | ||
| /// <param name="FailureDetails">The failure details when <paramref name="Failed"/> is true, otherwise <c>null</c>.</param> | ||
| /// <remarks> | ||
| /// Mirrors the <c>ChildWorkflowResult</c> type in the Go and Python SDKs. | ||
| /// </remarks> | ||
| public sealed record ChildWorkflowResult( | ||
| string Name, | ||
| bool Started, | ||
| bool Completed, | ||
| bool Failed, | ||
| string? Output, | ||
| WorkflowTaskFailureDetails? FailureDetails); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // ------------------------------------------------------------------------ | ||
| // Copyright 2026 The Dapr Authors | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| // ------------------------------------------------------------------------ | ||
|
|
||
| namespace Dapr.Workflow; | ||
|
|
||
| using System; | ||
|
|
||
| /// <summary> | ||
| /// Thrown when a query against propagated workflow history finds no match. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Raised by <see cref="PropagatedHistory.GetLastWorkflowByName"/>, | ||
| /// <see cref="WorkflowResult.GetLastActivityByName"/>, and | ||
| /// <see cref="WorkflowResult.GetLastChildWorkflowByName"/> when the requested | ||
| /// name is not present in the propagated history chain. Use the plural | ||
| /// <c>Get*sByName</c> variants if you want an empty-list result instead. | ||
| /// </remarks> | ||
| public sealed class PropagationNotFoundException : Exception | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of <see cref="PropagationNotFoundException"/>. | ||
| /// </summary> | ||
| public PropagationNotFoundException() | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of <see cref="PropagationNotFoundException"/> with a message. | ||
| /// </summary> | ||
| /// <param name="message">The exception message.</param> | ||
| public PropagationNotFoundException(string message) : base(message) | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of <see cref="PropagationNotFoundException"/> with a message | ||
| /// and inner exception. | ||
| /// </summary> | ||
| /// <param name="message">The exception message.</param> | ||
| /// <param name="innerException">The underlying cause.</param> | ||
| public PropagationNotFoundException(string message, Exception innerException) : base(message, innerException) | ||
| { | ||
| } | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The protos describe the type of history event returned. Is this not something meaningful to provide to the user?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lifecycle stage is captured by
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's premature to think that this might be the final state of the lifecycle functionality. A wall of flags in which one is only ever true seems cognitively confusing. I can anticipate developers preemptively asking if there's ever a situation in which more than one flag could ever be true at once - as this reflects a specific named lifecycle, no, in which case, it feels like a simpler effect here is to simply reflect the lifecycle enums of the original implementation. They needn't all be provided as only a subset are ever exposed here, but the idea is to think about how the SDK will be consumed most easily by developers, not just what's one of many ways to expose the concept. Further, should additional lifecycle events be added, a wall of flag evaluations must be extended to properly rule out new lifecycle flags. But if the new enums aren't necessary, they can simply be ignored. All told, let's use enums instead of flags.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushing back here:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a I kept the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The design decisions behind the other SDKs isn't of much concern to me - those maintainers should do what is most appropriate per their own language conventions and express the functionality in ways that make it easier for their developers to consume their SDKs. I weigh the perceived value of parity with other SDKs with the cost of expanding the public API and having to maintain two equivalent things going forward. I don't think a wall of flags is a developer-friendly way to communicate the state of something, so I appreciate you restoring the enum here. Please modify the visibility of the flag fields to make them internal - should there ever be a need to make them publicly accessible, we can always introduce them later, but I'd rather this remain internal and not a part of the public API today. |
This file was deleted.
|
WhitWaldo marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,68 +18,112 @@ namespace Dapr.Workflow; | |
| using System.Linq; | ||
|
|
||
| /// <summary> | ||
| /// Contains the workflow history that was propagated from ancestor workflow instances. | ||
| /// Each entry corresponds to a single ancestor's history. | ||
| /// Workflow history propagated from a parent workflow to a child workflow or activity. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// A workflow receives propagated history when it is scheduled with a | ||
| /// <see cref="HistoryPropagationScope"/> other than <see cref="HistoryPropagationScope.None"/>. | ||
| /// Use <see cref="WorkflowContext.GetPropagatedHistory"/> to retrieve the propagated history | ||
| /// inside a workflow implementation. | ||
| /// A propagated history is composed of one or more chunks, each owned by a distinct | ||
| /// workflow instance. Chunks preserve execution order: index 0 is the oldest ancestor, | ||
| /// the last chunk is the immediate parent. | ||
| /// <para> | ||
| /// Use the <c>Get*</c> methods to walk the chain by app, instance, or workflow name. | ||
| /// Mirrors the <c>PropagatedHistory</c> type in the Go and Python SDKs. | ||
| /// </para> | ||
| /// </remarks> | ||
| public sealed class PropagatedHistory | ||
| { | ||
| private readonly IReadOnlyList<PropagatedHistoryEntry> _entries; | ||
| private readonly IReadOnlyList<WorkflowResult> _workflows; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of <see cref="PropagatedHistory"/> with the given entries. | ||
| /// Initializes a new <see cref="PropagatedHistory"/> from the given workflow chunks. | ||
| /// </summary> | ||
| /// <param name="entries">The propagated history entries from ancestor workflows.</param> | ||
| public PropagatedHistory(IReadOnlyList<PropagatedHistoryEntry> entries) | ||
| /// <param name="workflows"> | ||
| /// Workflow chunks in execution order (ancestor first, immediate parent last). | ||
| /// </param> | ||
| public PropagatedHistory(IReadOnlyList<WorkflowResult> workflows) | ||
| { | ||
| _entries = entries ?? throw new ArgumentNullException(nameof(entries)); | ||
| _workflows = workflows ?? throw new ArgumentNullException(nameof(workflows)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the ordered list of propagated history entries. | ||
| /// The first entry corresponds to the immediate parent workflow; subsequent entries | ||
| /// correspond to progressively older ancestors when <see cref="HistoryPropagationScope.Lineage"/> is used. | ||
| /// Returns every workflow chunk in the chain, in execution order | ||
| /// (ancestor first, immediate parent last). | ||
| /// </summary> | ||
| public IReadOnlyList<PropagatedHistoryEntry> Entries => _entries; | ||
| public IReadOnlyList<WorkflowResult> GetWorkflows() => _workflows; | ||
|
|
||
| /// <summary> | ||
| /// Returns a new <see cref="PropagatedHistory"/> containing only entries from the specified App ID. | ||
| /// Returns an ordered, deduplicated list of app IDs in the propagated chain. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "chain" -> "list of propagated history events"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done — applied verbatim.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we know for a fact that propagated history will never include anything other than workflows? I don't know that and we have no roadmap indicating if this feature will evolve in the future. As this is a public method, I would rather retain the more generic "GetEntries()" name to keep our option open without introducing future breaking changes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushing back here too:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed |
||
| /// </summary> | ||
| /// <param name="appId">The Dapr App ID to filter by.</param> | ||
| /// <returns>A filtered <see cref="PropagatedHistory"/> instance.</returns> | ||
| public PropagatedHistory FilterByAppId(string appId) | ||
| public IReadOnlyList<string> GetAppIds() | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(appId); | ||
| return new PropagatedHistory( | ||
| _entries.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase)).ToList()); | ||
| var seen = new HashSet<string>(StringComparer.Ordinal); | ||
|
nelson-parente marked this conversation as resolved.
Outdated
|
||
| var result = new List<string>(_workflows.Count); | ||
| foreach (var workflow in _workflows) | ||
| { | ||
| if (seen.Add(workflow.AppId)) | ||
| { | ||
| result.Add(workflow.AppId); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a new <see cref="PropagatedHistory"/> containing only the entry with the specified instance ID. | ||
| /// Returns every workflow whose name matches, in execution order. Useful when the | ||
| /// chain contains the same name more than once (e.g. recursion or ContinueAsNew). | ||
|
WhitWaldo marked this conversation as resolved.
Outdated
|
||
| /// </summary> | ||
| /// <param name="instanceId">The workflow instance ID to filter by.</param> | ||
| /// <returns>A filtered <see cref="PropagatedHistory"/> instance.</returns> | ||
| public PropagatedHistory FilterByInstanceId(string instanceId) | ||
| /// <param name="name">The workflow name to filter by.</param> | ||
| /// <returns>An empty list when no match is found.</returns> | ||
| public IReadOnlyList<WorkflowResult> GetWorkflowsByName(string name) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); | ||
| return new PropagatedHistory( | ||
| _entries.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal)).ToList()); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(name); | ||
| return _workflows | ||
| .Where(w => string.Equals(w.Name, name, StringComparison.Ordinal)) | ||
| .ToList(); | ||
|
WhitWaldo marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the most recent workflow in the chain whose name matches. | ||
| /// </summary> | ||
| /// <param name="name">The workflow name to look up.</param> | ||
| /// <returns>The last matching workflow chunk.</returns> | ||
| /// <exception cref="PropagationNotFoundException">No workflow with the given name is present in the chain.</exception> | ||
| public WorkflowResult GetLastWorkflowByName(string name) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(name); | ||
| var matches = GetWorkflowsByName(name); | ||
| if (matches.Count == 0) | ||
| { | ||
| throw new PropagationNotFoundException($"no workflow named '{name}' in propagated history"); | ||
| } | ||
|
|
||
| return matches[^1]; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns every workflow chunk produced by the given app, in execution order. | ||
| /// </summary> | ||
| /// <param name="appId">The Dapr App ID to filter by.</param> | ||
| /// <returns>An empty list when no match is found.</returns> | ||
| public IReadOnlyList<WorkflowResult> GetWorkflowsByAppId(string appId) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(appId); | ||
| return _workflows | ||
| .Where(w => string.Equals(w.AppId, appId, StringComparison.Ordinal)) | ||
| .ToList(); | ||
|
nelson-parente marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a new <see cref="PropagatedHistory"/> containing only entries for the specified workflow name. | ||
| /// Returns every workflow chunk produced by the given instance, in execution order. | ||
|
WhitWaldo marked this conversation as resolved.
Outdated
|
||
| /// Usually a single entry, except when the same instance reappears via ContinueAsNew. | ||
| /// </summary> | ||
| /// <param name="workflowName">The workflow name to filter by.</param> | ||
| /// <returns>A filtered <see cref="PropagatedHistory"/> instance.</returns> | ||
| public PropagatedHistory FilterByWorkflowName(string workflowName) | ||
| /// <param name="instanceId">The workflow instance ID to filter by.</param> | ||
| /// <returns>An empty list when no match is found.</returns> | ||
| public IReadOnlyList<WorkflowResult> GetWorkflowsByInstanceId(string instanceId) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(workflowName); | ||
| return new PropagatedHistory( | ||
| _entries.Where(e => string.Equals(e.WorkflowName, workflowName, StringComparison.Ordinal)).ToList()); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); | ||
| return _workflows | ||
| .Where(w => string.Equals(w.InstanceId, instanceId, StringComparison.Ordinal)) | ||
| .ToList(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.