Skip to content

Commit 09558e6

Browse files
WhitWaldoCopilot
andauthored
Feature: Workflow history propagation (#1802)
* Updating Workflow protos * Implemented changes to support workflow history propagation * Added unit and integration tests * Marking tests for features coming with the 1.18 runtime with appropriate test filter flags Merging pending fix for backwards compatibility with Jobs SDK Signed-off-by: Whit Waldo <whit.waldo@innovian.net> --------- Signed-off-by: Whit Waldo <whit.waldo@innovian.net> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 0d7cacb commit 09558e6

19 files changed

Lines changed: 1326 additions & 20 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2026 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
namespace Dapr.Workflow;
15+
16+
/// <summary>
17+
/// Identifies the kind of a workflow history event returned in propagated history.
18+
/// </summary>
19+
public enum HistoryEventKind
20+
{
21+
/// <summary>
22+
/// Unknown or unsupported event type.
23+
/// </summary>
24+
Unknown = 0,
25+
26+
/// <summary>
27+
/// The workflow execution started.
28+
/// </summary>
29+
ExecutionStarted,
30+
31+
/// <summary>
32+
/// The workflow execution completed.
33+
/// </summary>
34+
ExecutionCompleted,
35+
36+
/// <summary>
37+
/// The workflow execution was terminated.
38+
/// </summary>
39+
ExecutionTerminated,
40+
41+
/// <summary>
42+
/// An activity task was scheduled.
43+
/// </summary>
44+
TaskScheduled,
45+
46+
/// <summary>
47+
/// An activity task completed successfully.
48+
/// </summary>
49+
TaskCompleted,
50+
51+
/// <summary>
52+
/// An activity task failed.
53+
/// </summary>
54+
TaskFailed,
55+
56+
/// <summary>
57+
/// A child workflow instance was created.
58+
/// </summary>
59+
SubOrchestrationInstanceCreated,
60+
61+
/// <summary>
62+
/// A child workflow instance completed successfully.
63+
/// </summary>
64+
SubOrchestrationInstanceCompleted,
65+
66+
/// <summary>
67+
/// A child workflow instance failed.
68+
/// </summary>
69+
SubOrchestrationInstanceFailed,
70+
71+
/// <summary>
72+
/// A durable timer was created.
73+
/// </summary>
74+
TimerCreated,
75+
76+
/// <summary>
77+
/// A durable timer fired.
78+
/// </summary>
79+
TimerFired,
80+
81+
/// <summary>
82+
/// The orchestrator started a processing turn.
83+
/// </summary>
84+
OrchestratorStarted,
85+
86+
/// <summary>
87+
/// The orchestrator completed a processing turn.
88+
/// </summary>
89+
OrchestratorCompleted,
90+
91+
/// <summary>
92+
/// An event was sent to another workflow instance.
93+
/// </summary>
94+
EventSent,
95+
96+
/// <summary>
97+
/// An external event was raised for this workflow instance.
98+
/// </summary>
99+
EventRaised,
100+
101+
/// <summary>
102+
/// The workflow continued as new.
103+
/// </summary>
104+
ContinueAsNew,
105+
106+
/// <summary>
107+
/// The workflow execution was suspended.
108+
/// </summary>
109+
ExecutionSuspended,
110+
111+
/// <summary>
112+
/// The workflow execution was resumed.
113+
/// </summary>
114+
ExecutionResumed
115+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2026 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
namespace Dapr.Workflow;
15+
16+
/// <summary>
17+
/// Defines the scope of workflow history that is propagated to a child workflow.
18+
/// </summary>
19+
public enum HistoryPropagationScope
20+
{
21+
/// <summary>
22+
/// No history is propagated to child workflows. This is the default behavior.
23+
/// </summary>
24+
None = 0,
25+
26+
/// <summary>
27+
/// Only the calling workflow's own history events are propagated to the child.
28+
/// Ancestor history is excluded, acting as a trust boundary.
29+
/// </summary>
30+
OwnHistory = 1,
31+
32+
/// <summary>
33+
/// The calling workflow's history and all ancestor history (the full lineage) is propagated.
34+
/// </summary>
35+
Lineage = 2,
36+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2026 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
namespace Dapr.Workflow;
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
20+
/// <summary>
21+
/// Contains the workflow history that was propagated from ancestor workflow instances.
22+
/// Each entry corresponds to a single ancestor's history.
23+
/// </summary>
24+
/// <remarks>
25+
/// A workflow receives propagated history when it is scheduled with a
26+
/// <see cref="HistoryPropagationScope"/> other than <see cref="HistoryPropagationScope.None"/>.
27+
/// Use <see cref="WorkflowContext.GetPropagatedHistory"/> to retrieve the propagated history
28+
/// inside a workflow implementation.
29+
/// </remarks>
30+
public sealed class PropagatedHistory
31+
{
32+
private readonly IReadOnlyList<PropagatedHistoryEntry> _entries;
33+
34+
/// <summary>
35+
/// Initializes a new instance of <see cref="PropagatedHistory"/> with the given entries.
36+
/// </summary>
37+
/// <param name="entries">The propagated history entries from ancestor workflows.</param>
38+
public PropagatedHistory(IReadOnlyList<PropagatedHistoryEntry> entries)
39+
{
40+
_entries = entries ?? throw new ArgumentNullException(nameof(entries));
41+
}
42+
43+
/// <summary>
44+
/// Gets the ordered list of propagated history entries.
45+
/// The first entry corresponds to the immediate parent workflow; subsequent entries
46+
/// correspond to progressively older ancestors when <see cref="HistoryPropagationScope.Lineage"/> is used.
47+
/// </summary>
48+
public IReadOnlyList<PropagatedHistoryEntry> Entries => _entries;
49+
50+
/// <summary>
51+
/// Returns a new <see cref="PropagatedHistory"/> containing only entries from the specified App ID.
52+
/// </summary>
53+
/// <param name="appId">The Dapr App ID to filter by.</param>
54+
/// <returns>A filtered <see cref="PropagatedHistory"/> instance.</returns>
55+
public PropagatedHistory FilterByAppId(string appId)
56+
{
57+
ArgumentException.ThrowIfNullOrWhiteSpace(appId);
58+
return new PropagatedHistory(
59+
_entries.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase)).ToList());
60+
}
61+
62+
/// <summary>
63+
/// Returns a new <see cref="PropagatedHistory"/> containing only the entry with the specified instance ID.
64+
/// </summary>
65+
/// <param name="instanceId">The workflow instance ID to filter by.</param>
66+
/// <returns>A filtered <see cref="PropagatedHistory"/> instance.</returns>
67+
public PropagatedHistory FilterByInstanceId(string instanceId)
68+
{
69+
ArgumentException.ThrowIfNullOrWhiteSpace(instanceId);
70+
return new PropagatedHistory(
71+
_entries.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal)).ToList());
72+
}
73+
74+
/// <summary>
75+
/// Returns a new <see cref="PropagatedHistory"/> containing only entries for the specified workflow name.
76+
/// </summary>
77+
/// <param name="workflowName">The workflow name to filter by.</param>
78+
/// <returns>A filtered <see cref="PropagatedHistory"/> instance.</returns>
79+
public PropagatedHistory FilterByWorkflowName(string workflowName)
80+
{
81+
ArgumentException.ThrowIfNullOrWhiteSpace(workflowName);
82+
return new PropagatedHistory(
83+
_entries.Where(e => string.Equals(e.WorkflowName, workflowName, StringComparison.Ordinal)).ToList());
84+
}
85+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2026 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
namespace Dapr.Workflow;
15+
16+
using System.Collections.Generic;
17+
18+
/// <summary>
19+
/// Represents a segment of propagated workflow history originating from a single ancestor workflow instance.
20+
/// </summary>
21+
/// <param name="AppId">The Dapr App ID of the application that ran the ancestor workflow.</param>
22+
/// <param name="InstanceId">The instance ID of the ancestor workflow.</param>
23+
/// <param name="WorkflowName">The name of the ancestor workflow.</param>
24+
/// <param name="Events">The ordered list of history events from the ancestor workflow.</param>
25+
public sealed record PropagatedHistoryEntry(
26+
string AppId,
27+
string InstanceId,
28+
string WorkflowName,
29+
IReadOnlyList<PropagatedHistoryEvent> Events);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2026 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
namespace Dapr.Workflow;
15+
16+
using System;
17+
18+
/// <summary>
19+
/// Represents a single event in a propagated workflow history segment.
20+
/// </summary>
21+
/// <param name="EventId">The unique event ID within the workflow instance history.</param>
22+
/// <param name="Kind">The kind of history event.</param>
23+
/// <param name="Timestamp">The UTC timestamp when the event occurred.</param>
24+
public sealed record PropagatedHistoryEvent(int EventId, HistoryEventKind Kind, DateTimeOffset Timestamp);

src/Dapr.Workflow.Abstractions/WorkflowContext.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,14 @@ public virtual async Task<T> WaitForExternalEventAsync<T>(string eventName, Time
169169
Task winner = await Task.WhenAny(timeoutTask, externalEventTask);
170170
if (winner == externalEventTask)
171171
{
172-
timerCts.Cancel();
172+
await timerCts.CancelAsync();
173173
}
174174
else
175175
{
176-
eventCts.Cancel();
176+
await eventCts.CancelAsync();
177177
}
178178

179-
// This will either return the received value or throw if the task was cancelled.
179+
// This will either return the received value or throw if the task was canceled.
180180
return await externalEventTask;
181181
}
182182

@@ -330,4 +330,27 @@ public virtual Task CallChildWorkflowAsync(
330330
/// </remarks>
331331
/// <returns>The new <see cref="Guid"/> value.</returns>
332332
public abstract Guid NewGuid();
333+
334+
/// <summary>
335+
/// Gets the workflow history that was propagated from ancestor workflow instances, or <c>null</c>
336+
/// if no history was propagated to this workflow.
337+
/// </summary>
338+
/// <remarks>
339+
/// <para>
340+
/// A workflow receives propagated history when it is scheduled as a child workflow and the parent
341+
/// specified a <see cref="HistoryPropagationScope"/> other than <see cref="HistoryPropagationScope.None"/>.
342+
/// </para>
343+
/// <para>
344+
/// Use <see cref="PropagatedHistory.FilterByAppId"/>, <see cref="PropagatedHistory.FilterByInstanceId"/>,
345+
/// or <see cref="PropagatedHistory.FilterByWorkflowName"/> to narrow down the returned entries.
346+
/// </para>
347+
/// <para>
348+
/// This method always returns the same value regardless of whether the workflow is replaying.
349+
/// </para>
350+
/// </remarks>
351+
/// <returns>
352+
/// A <see cref="PropagatedHistory"/> containing entries from ancestor workflows,
353+
/// or <c>null</c> if no history was propagated to this workflow instance.
354+
/// </returns>
355+
public abstract PropagatedHistory? GetPropagatedHistory();
333356
}

src/Dapr.Workflow.Abstractions/WorkflowTaskOptions.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@ public record WorkflowTaskOptions(WorkflowRetryPolicy? RetryPolicy = null, strin
2626
/// <param name="InstanceId">The instance ID to use for the child workflow.</param>
2727
/// <param name="RetryPolicy">The child workflow's retry policy.</param>
2828
/// <param name="TargetAppId">The App ID indicating the app in which to find the named child workflow to run.</param>
29+
/// <param name="PropagationScope">
30+
/// Determines which ancestor history events are propagated to the child workflow.
31+
/// Defaults to <see cref="HistoryPropagationScope.None"/>, meaning no history is propagated.
32+
/// </param>
2933
public record ChildWorkflowTaskOptions(
30-
string? InstanceId = null,
31-
WorkflowRetryPolicy? RetryPolicy = null,
32-
string? TargetAppId = null) : WorkflowTaskOptions(RetryPolicy, TargetAppId);
34+
string? InstanceId = null,
35+
WorkflowRetryPolicy? RetryPolicy = null,
36+
string? TargetAppId = null,
37+
HistoryPropagationScope PropagationScope = HistoryPropagationScope.None) : WorkflowTaskOptions(RetryPolicy, TargetAppId)
38+
{
39+
/// <summary>
40+
/// Returns a new <see cref="ChildWorkflowTaskOptions"/> with the specified history propagation scope.
41+
/// </summary>
42+
/// <param name="scope">The propagation scope to apply.</param>
43+
/// <returns>A new options instance with the propagation scope set.</returns>
44+
public ChildWorkflowTaskOptions WithHistoryPropagation(HistoryPropagationScope scope) =>
45+
this with { PropagationScope = scope };
46+
}

0 commit comments

Comments
 (0)