Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Temporalio/Client/TemporalClient.Workflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ await Client.Options.DataConverter.ToPayloadsAsync(
throw new WorkflowQueryRejectedException(resp.QueryRejected.Status);
}

if (resp.QueryResult == null)
// Use default value if no result present
if (resp.QueryResult == null || resp.QueryResult.Payloads_.Count == 0)
{
throw new InvalidOperationException("No result present");
return default!;
}
return await Client.Options.DataConverter.ToSingleValueAsync<TResult>(
resp.QueryResult.Payloads_).ConfigureAwait(false);
Expand Down
10 changes: 3 additions & 7 deletions src/Temporalio/Client/WorkflowHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,12 @@ public virtual async Task<TResult> GetResultAsync<TResult>(
histRunId = compAttr.NewExecutionRunId;
break;
}
// Ignore return if they didn't want it
if (typeof(TResult) == typeof(ValueTuple))
// Use default if they are ignoring result or payload not present
if (typeof(TResult) == typeof(ValueTuple) ||
compAttr.Result == null || compAttr.Result.Payloads_.Count == 0)
{
return default!;
}
// Otherwise we expect a single payload
if (compAttr.Result == null)
{
throw new InvalidOperationException("No result present");
}
return await Client.Options.DataConverter.ToSingleValueAsync<TResult>(
compAttr.Result.Payloads_).ConfigureAwait(false);
case HistoryEvent.AttributesOneofCase.WorkflowExecutionFailedEventAttributes:
Expand Down
5 changes: 3 additions & 2 deletions src/Temporalio/Client/WorkflowUpdateHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ public virtual async Task<TResult> GetResultAsync<TResult>(RpcOptions? rpcOption
}
else if (KnownOutcome.Success is { } success)
{
// Ignore return if they didn't want it
if (typeof(TResult) == typeof(ValueTuple))
// Use default if they are ignoring result or payload not present
if (typeof(TResult) == typeof(ValueTuple) ||
success == null || success.Payloads_.Count == 0)
{
return default!;
}
Expand Down
15 changes: 5 additions & 10 deletions src/Temporalio/Worker/WorkflowInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2219,11 +2219,6 @@ public override Task<ChildWorkflowHandle<TWorkflow, TResult>> StartChildWorkflow
switch (completeRes.StatusCase)
{
case ChildWorkflowResult.StatusOneofCase.Completed:
// We expect a single payload
if (completeRes.Completed.Result == null)
{
throw new InvalidOperationException("No child result present");
}
handle.CompletionSource.SetResult(completeRes.Completed.Result);
break;
case ChildWorkflowResult.StatusOneofCase.Failed:
Expand Down Expand Up @@ -2337,8 +2332,8 @@ private Task<TResult> ExecuteActivityInternalAsync<TResult>(
switch (res.StatusCase)
{
case ActivityResolution.StatusOneofCase.Completed:
// Ignore result if they didn't want it
if (typeof(TResult) == typeof(ValueTuple))
// Use default if they are ignoring result or payload not present
if (typeof(TResult) == typeof(ValueTuple) || res.Completed.Result == null)
{
return default!;
}
Expand Down Expand Up @@ -2408,14 +2403,14 @@ public ChildWorkflowHandleImpl(
/// <summary>
/// Gets the source for the resulting payload of the child.
/// </summary>
internal TaskCompletionSource<Payload> CompletionSource { get; } = new();
internal TaskCompletionSource<Payload?> CompletionSource { get; } = new();

/// <inheritdoc />
public override async Task<TLocalResult> GetResultAsync<TLocalResult>()
{
var payload = await CompletionSource.Task.ConfigureAwait(true);
// Ignore if they are ignoring result
if (typeof(TLocalResult) == typeof(ValueTuple))
// Use default if they are ignoring result or payload not present
if (typeof(TLocalResult) == typeof(ValueTuple) || payload == null)
{
return default!;
}
Expand Down
66 changes: 66 additions & 0 deletions tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6626,6 +6626,72 @@ await replayer.ReplayWorkflowAsync(WorkflowHistory.FromJson(
Assert.Equal(new List<string> { "one", "two", "four", "three" }, MultiWaitConditionPreSdkFlagWorkflow.Stages);
}

[Workflow]
public class PayloadMissingWorkflow
{
[WorkflowRun]
public async Task<string?> RunAsync(bool returnImmediately)
{
if (returnImmediately)
{
return null;
}
// Run child that returns immediately, run activity that does the same
await Workflow.ExecuteChildWorkflowAsync<string?>(
"PayloadMissingWorkflow",
new object?[] { true });
await Workflow.ExecuteActivityAsync<string?>(
"DoSomething",
Array.Empty<object?>(),
new() { StartToCloseTimeout = TimeSpan.FromSeconds(30) });
return null;
}

[Activity]
public static string? DoSomething() => null;
}

[Fact]
public async Task ExecuteWorkflowAsync_PayloadMissing_StillWorks()
{
// This test confirms that the absence of a payload inside activity and child workflow
// result is ok. This can happen when calling an activity or child in another SDK like Go.

// Run workflow and get handle
var handle = await ExecuteWorkerAsync<PayloadMissingWorkflow, WorkflowHandle>(
async worker =>
{
var handle = await Client.StartWorkflowAsync(
(PayloadMissingWorkflow wf) => wf.RunAsync(false),
new($"workflow-{Guid.NewGuid()}", worker.Options.TaskQueue!));
await handle.GetResultAsync();
return handle;
},
new TemporalWorkerOptions().AddAllActivities<PayloadMissingWorkflow>(null));

// Get history and confirm it can replay
var replayer = new WorkflowReplayer(
new WorkflowReplayerOptions().AddWorkflow<PayloadMissingWorkflow>());
var history = await handle.FetchHistoryAsync();
await replayer.ReplayWorkflowAsync(history);

// Now complete remove the activity and child complete payloads. This used to break.
foreach (var evt in history.Events)
{
if (evt.ChildWorkflowExecutionCompletedEventAttributes is { } childEvent)
{
Assert.NotNull(childEvent.Result);
childEvent.Result = null;
}
else if (evt.ActivityTaskCompletedEventAttributes is { } actEvent)
{
Assert.NotNull(actEvent.Result);
actEvent.Result = null;
}
}
await replayer.ReplayWorkflowAsync(history);
}

internal static Task AssertTaskFailureContainsEventuallyAsync(
WorkflowHandle handle, string messageContains)
{
Expand Down
Loading