Skip to content

Commit 855047e

Browse files
authored
Update with start (#381)
Fixes #346
1 parent d073176 commit 855047e

16 files changed

+1314
-152
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,9 @@ Some things to note about the above code:
541541
* A shortcut extension `ExecuteWorkflowAsync` is available that is just `StartWorkflowAsync` + `GetResultAsync`.
542542
* `SignalWithStart` method is present on the workflow options to make the workflow call a signal-with-start call which
543543
means it will only start the workflow if it's not running, but send a signal to it regardless.
544+
* Separate `StartUpdateWithStartWorkflowAsync` and `ExecuteUpdateWithStartWorkflowAsync` methods are present on the
545+
client to make the workflow call an update-with-start call which means it may start the workflow if it's not running,
546+
but perform an update on it regardless.
544547

545548
#### Invoking Activities
546549

src/Temporalio.Extensions.OpenTelemetry/TracingInterceptor.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,27 @@ protected virtual IDictionary<string, Payload> HeadersFromContext(
124124
return Enumerable.Empty<KeyValuePair<string, object?>>();
125125
}
126126

127+
/// <summary>
128+
/// Create tag collection for the given workflow and update ID.
129+
/// </summary>
130+
/// <param name="workflowId">Workflow ID.</param>
131+
/// <param name="updateId">Update ID.</param>
132+
/// <returns>Tags.</returns>
133+
protected virtual IEnumerable<KeyValuePair<string, object?>> CreateUpdateTags(
134+
string workflowId, string? updateId)
135+
{
136+
var ret = new List<KeyValuePair<string, object?>>(2);
137+
if (Options.TagNameWorkflowId is string wfName)
138+
{
139+
ret.Add(new(wfName, workflowId));
140+
}
141+
if (Options.TagNameUpdateId is string updateName && updateId is { } nonNullUpdateId)
142+
{
143+
ret.Add(new(updateName, nonNullUpdateId));
144+
}
145+
return ret;
146+
}
147+
127148
/// <summary>
128149
/// Create tag collection from the current workflow environment. Must be called within a
129150
/// workflow.
@@ -208,6 +229,48 @@ public override async Task<WorkflowHandle<TWorkflow, TResult>> StartWorkflowAsyn
208229
}
209230
}
210231

232+
public override async Task<WorkflowUpdateHandle<TUpdateResult>> StartUpdateWithStartWorkflowAsync<TUpdateResult>(
233+
StartUpdateWithStartWorkflowInput input)
234+
{
235+
// Ignore if for some reason the start operation is not set by this interceptor
236+
if (input.Options.StartWorkflowOperation == null)
237+
{
238+
return await base.StartUpdateWithStartWorkflowAsync<TUpdateResult>(input).ConfigureAwait(false);
239+
}
240+
241+
using (var activity = ClientSource.StartActivity(
242+
$"UpdateWithStartWorkflow:{input.Options.StartWorkflowOperation.Workflow}",
243+
kind: ActivityKind.Client,
244+
parentContext: default,
245+
tags: root.CreateUpdateTags(
246+
workflowId: input.Options.StartWorkflowOperation.Options.Id!,
247+
updateId: input.Options.Id)))
248+
{
249+
// We want the header on _both_ start and update
250+
if (HeadersFromContext(input.Headers) is Dictionary<string, Payload> updateHeaders)
251+
{
252+
input = input with { Headers = updateHeaders };
253+
}
254+
if (HeadersFromContext(input.Options.StartWorkflowOperation.Headers) is Dictionary<string, Payload> startHeaders)
255+
{
256+
// We copy the operation but still mutate the existing headers. This is
257+
// similar to what is done by other interceptors (they copy the input
258+
// object but still mutate the original header dictionary if there).
259+
input.Options.StartWorkflowOperation = (WithStartWorkflowOperation)input.Options.StartWorkflowOperation.Clone();
260+
input.Options.StartWorkflowOperation.Headers = startHeaders;
261+
}
262+
try
263+
{
264+
return await base.StartUpdateWithStartWorkflowAsync<TUpdateResult>(input).ConfigureAwait(false);
265+
}
266+
catch (Exception e)
267+
{
268+
RecordExceptionWithStatus(activity, e);
269+
throw;
270+
}
271+
}
272+
}
273+
211274
public override async Task SignalWorkflowAsync(SignalWorkflowInput input)
212275
{
213276
using (var activity = ClientSource.StartActivity(
@@ -263,7 +326,7 @@ public override async Task<WorkflowUpdateHandle<TResult>> StartWorkflowUpdateAsy
263326
$"UpdateWorkflow:{input.Update}",
264327
kind: ActivityKind.Client,
265328
parentContext: default,
266-
tags: root.CreateWorkflowTags(input.Id)))
329+
tags: root.CreateUpdateTags(workflowId: input.Id, updateId: input.Options.Id)))
267330
{
268331
if (HeadersFromContext(input.Headers) is Dictionary<string, Payload> headers)
269332
{

src/Temporalio.Extensions.OpenTelemetry/TracingInterceptorOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public class TracingInterceptorOptions : ICloneable
3333
/// </summary>
3434
public string? TagNameActivityId { get; set; } = "temporalActivityID";
3535

36+
/// <summary>
37+
/// Gets or sets the tag name for update IDs. If null, no tag is created.
38+
/// </summary>
39+
public string? TagNameUpdateId { get; set; } = "temporalUpdateID";
40+
3641
/// <summary>
3742
/// Create a shallow copy of these options.
3843
/// </summary>

src/Temporalio/Client/ITemporalClient.Workflow.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,84 @@ WorkflowHandle<TWorkflow> GetWorkflowHandle<TWorkflow>(
9292
WorkflowHandle<TWorkflow, TResult> GetWorkflowHandle<TWorkflow, TResult>(
9393
string id, string? runId = null, string? firstExecutionRunId = null);
9494

95+
/// <summary>
96+
/// Start an update via a call to a WorkflowUpdate attributed method, possibly starting the
97+
/// workflow at the same time. Note that in some cases this call may fail but the workflow
98+
/// will still be started.
99+
/// </summary>
100+
/// <typeparam name="TWorkflow">Workflow class type.</typeparam>
101+
/// <param name="updateCall">Invocation of workflow update method.</param>
102+
/// <param name="options">Update options. Currently <c>WaitForStage</c> is required.</param>
103+
/// <returns>Workflow update handle.</returns>
104+
/// <exception cref="ArgumentException">Invalid run call or options.</exception>
105+
/// <exception cref="Exceptions.WorkflowAlreadyStartedException">
106+
/// Workflow was already started according to ID reuse and conflict policy.
107+
/// </exception>
108+
/// <exception cref="Exceptions.RpcException">Server-side error.</exception>
109+
/// <remarks>WARNING: Workflow update with start is experimental and APIs may change.
110+
/// </remarks>
111+
Task<WorkflowUpdateHandle> StartUpdateWithStartWorkflowAsync<TWorkflow>(
112+
Expression<Func<TWorkflow, Task>> updateCall,
113+
WorkflowStartUpdateWithStartOptions options);
114+
115+
/// <summary>
116+
/// Start an update via a call to a WorkflowUpdate attributed method, possibly starting the
117+
/// workflow at the same time. Note that in some cases this call may fail but the workflow
118+
/// will still be started.
119+
/// </summary>
120+
/// <typeparam name="TWorkflow">Workflow class type.</typeparam>
121+
/// <typeparam name="TUpdateResult">Update result type.</typeparam>
122+
/// <param name="updateCall">Invocation of workflow update method.</param>
123+
/// <param name="options">Update options. Currently <c>WaitForStage</c> is required.</param>
124+
/// <returns>Workflow update handle.</returns>
125+
/// <exception cref="ArgumentException">Invalid run call or options.</exception>
126+
/// <exception cref="Exceptions.WorkflowAlreadyStartedException">
127+
/// Workflow was already started according to ID reuse and conflict policy.
128+
/// </exception>
129+
/// <exception cref="Exceptions.RpcException">Server-side error.</exception>
130+
/// <remarks>WARNING: Workflow update with start is experimental and APIs may change.
131+
/// </remarks>
132+
Task<WorkflowUpdateHandle<TUpdateResult>> StartUpdateWithStartWorkflowAsync<TWorkflow, TUpdateResult>(
133+
Expression<Func<TWorkflow, Task<TUpdateResult>>> updateCall,
134+
WorkflowStartUpdateWithStartOptions options);
135+
136+
/// <summary>
137+
/// Start an update using its name, possibly starting the workflow at the same time. Note
138+
/// that in some cases this call may fail but the workflow will still be started.
139+
/// </summary>
140+
/// <param name="update">Name of the update.</param>
141+
/// <param name="args">Arguments for the update.</param>
142+
/// <param name="options">Update options. Currently <c>WaitForStage</c> is required.</param>
143+
/// <returns>Workflow update handle.</returns>
144+
/// <exception cref="ArgumentException">Invalid run call or options.</exception>
145+
/// <exception cref="Exceptions.WorkflowAlreadyStartedException">
146+
/// Workflow was already started according to ID reuse and conflict policy.
147+
/// </exception>
148+
/// <exception cref="Exceptions.RpcException">Server-side error.</exception>
149+
/// <remarks>WARNING: Workflow update with start is experimental and APIs may change.
150+
/// </remarks>
151+
Task<WorkflowUpdateHandle> StartUpdateWithStartWorkflowAsync(
152+
string update, IReadOnlyCollection<object?> args, WorkflowStartUpdateWithStartOptions options);
153+
154+
/// <summary>
155+
/// Start an update using its name, possibly starting the workflow at the same time. Note
156+
/// that in some cases this call may fail but the workflow will still be started.
157+
/// </summary>
158+
/// <typeparam name="TUpdateResult">Update result type.</typeparam>
159+
/// <param name="update">Name of the update.</param>
160+
/// <param name="args">Arguments for the update.</param>
161+
/// <param name="options">Update options. Currently <c>WaitForStage</c> is required.</param>
162+
/// <returns>Workflow update handle.</returns>
163+
/// <exception cref="ArgumentException">Invalid run call or options.</exception>
164+
/// <exception cref="Exceptions.WorkflowAlreadyStartedException">
165+
/// Workflow was already started according to ID reuse and conflict policy.
166+
/// </exception>
167+
/// <exception cref="Exceptions.RpcException">Server-side error.</exception>
168+
/// <remarks>WARNING: Workflow update with start is experimental and APIs may change.
169+
/// </remarks>
170+
Task<WorkflowUpdateHandle<TUpdateResult>> StartUpdateWithStartWorkflowAsync<TUpdateResult>(
171+
string update, IReadOnlyCollection<object?> args, WorkflowStartUpdateWithStartOptions options);
172+
95173
#if NETCOREAPP3_0_OR_GREATER
96174
/// <summary>
97175
/// List workflows with the given query.

0 commit comments

Comments
 (0)