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
8 changes: 8 additions & 0 deletions src/Temporalio/Worker/WorkflowInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2572,6 +2572,14 @@ public override Task<NexusOperationHandle<TResult>> StartNexusOperationAsync<TRe
{
cmd.ScheduleToCloseTimeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(schedToCloseTimeout);
}
if (input.Options.ScheduleToStartTimeout is TimeSpan schedToStartTimeout)
{
cmd.ScheduleToStartTimeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(schedToStartTimeout);
}
if (input.Options.StartToCloseTimeout is TimeSpan startToCloseTimeout)
{
cmd.StartToCloseTimeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(startToCloseTimeout);
}
if (input.Headers is IDictionary<string, string> headers)
{
cmd.NexusHeader.Add(headers);
Expand Down
21 changes: 21 additions & 0 deletions src/Temporalio/Workflows/NexusOperationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,29 @@ public class NexusOperationOptions : ICloneable
/// <summary>
/// Gets or sets the schedule to close timeout.
/// </summary>
/// <remarks>Indicates how long the caller is willing to wait for operation completion.
/// Calls are retried internally by the server.</remarks>
public TimeSpan? ScheduleToCloseTimeout { get; set; }

/// <summary>
/// Gets or sets the schedule to start timeout.
/// </summary>
/// <remarks>Indicates how long the caller is willing to wait for the operation to be started (or completed if synchronous)
/// by the handler. If the operation is not started within this timeout, it will fail with TIMEOUT_TYPE_SCHEDULE_TO_START.
/// If not set or zero, no schedule-to-start timeout is enforced.
/// Requires server version 1.31.0 or later.</remarks>
public TimeSpan? ScheduleToStartTimeout { get; set; }

/// <summary>
/// Gets or sets the start to close timeout.
/// </summary>
/// <remarks>Indicates how long the caller is willing to wait for an asynchronous operation to complete after it has been
/// started. If the operation does not complete within this timeout after starting, it will fail with TIMEOUT_TYPE_START_TO_CLOSE.
/// Only applies to asynchronous operations. Synchronous operations ignore this timeout.
/// If not set or zero, no start-to-close timeout is enforced.
/// Requires server version 1.31.0 or later.</remarks>
public TimeSpan? StartToCloseTimeout { get; set; }

/// <summary>
/// Gets or sets the summary.
/// </summary>
Expand Down
72 changes: 71 additions & 1 deletion tests/Temporalio.Tests/Worker/NexusWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,9 @@ await Workflow.CreateNexusClient<IStringService>(endpoint).
svc => svc.DoSomething("some-name"),
new() { ScheduleToCloseTimeout = TimeSpan.FromSeconds(2) });
}));
Assert.IsType<TimeoutFailureException>(
var timeoutExc = Assert.IsType<TimeoutFailureException>(
Assert.IsType<NexusOperationFailureException>(exc.InnerException).InnerException);
Assert.Equal(TimeoutType.ScheduleToClose, timeoutExc.TimeoutType);
Comment thread
cursor[bot] marked this conversation as resolved.
// Also check that our cancel token is canceled for the proper reason
var ctx = await contextSource.Task;
Assert.True(await Task.Run(() => ctx.CancellationToken.WaitHandle.WaitOne(2000)));
Expand Down Expand Up @@ -338,6 +339,75 @@ await Workflow.CreateNexusClient<IStringService>(endpoint).
Assert.Equal(expectedSummary, actualSummary);
}

[Fact]
public async Task ExecuteNexusOperationAsync_ScheduleToStartTimeout_FailsAsExpected()
{
var contextSource = new TaskCompletionSource<OperationStartContext>();
var workerOptions = new TemporalWorkerOptions($"tq-{Guid.NewGuid()}").
AddNexusService(new HandlerFactoryStringService(() =>
OperationHandler.Sync<string, string>(async (ctx, name) =>
{
contextSource.SetResult(ctx);
try
{
await Task.Delay(40000, ctx.CancellationToken);
return "done";
}
catch (TaskCanceledException)
{
return "canceled";
}
})));
var endpoint = await CreateNexusEndpointAsync(workerOptions.TaskQueue!);
// Confirm the workflow fails with the timeout
var exc = await Assert.ThrowsAsync<WorkflowFailedException>(() =>
RunInWorkflowAsync(workerOptions, async () =>
{
await Workflow.CreateNexusClient<IStringService>(endpoint).
ExecuteNexusOperationAsync(
svc => svc.DoSomething("some-name"),
new() { ScheduleToStartTimeout = TimeSpan.FromSeconds(2) });
}));
var timeoutExc = Assert.IsType<TimeoutFailureException>(
Assert.IsType<NexusOperationFailureException>(exc.InnerException).InnerException);
Assert.Equal(TimeoutType.ScheduleToStart, timeoutExc.TimeoutType);
// Also check that our cancel token is canceled for the proper reason
var ctx = await contextSource.Task;
Assert.True(await Task.Run(() => ctx.CancellationToken.WaitHandle.WaitOne(2000)));
Assert.Equal("timed out", ctx.CancellationReason);
}

[Fact]
public async Task ExecuteNexusOperationAsync_StartToCloseTimeout_FailsAsExpected()
{
// Build a workflow-backed async operation that will never complete
var workerOptions = new TemporalWorkerOptions($"tq-{Guid.NewGuid()}").
AddNexusService(new HandlerFactoryStringService(() =>
WorkflowRunOperationHandler.FromHandleFactory(
(WorkflowRunOperationContext context, string input) =>
context.StartWorkflowAsync(
(WaitForeverWorkflow wf) => wf.RunAsync(input),
new() { Id = $"wf-{Guid.NewGuid()}" })))).
AddWorkflow<WaitForeverWorkflow>();
var endpoint = await CreateNexusEndpointAsync(workerOptions.TaskQueue!);
// Confirm the workflow fails with the timeout
var exc = await Assert.ThrowsAsync<WorkflowFailedException>(() =>
RunInWorkflowAsync(workerOptions, async () =>
{
await Workflow.CreateNexusClient<IStringService>(endpoint).
ExecuteNexusOperationAsync(
svc => svc.DoSomething("some-name"),
new()
{
ScheduleToStartTimeout = TimeSpan.FromSeconds(30),
StartToCloseTimeout = TimeSpan.FromSeconds(2),
});
}));
var timeoutExc = Assert.IsType<TimeoutFailureException>(
Assert.IsType<NexusOperationFailureException>(exc.InnerException).InnerException);
Assert.Equal(TimeoutType.StartToClose, timeoutExc.TimeoutType);
}

[Workflow]
public class WaitForSignalWorkflow
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Temporalio.Tests/WorkflowEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task InitializeAsync()
{
DevServerOptions = new()
{
DownloadVersion = "v1.6.1-server-1.31.0-150.0",
DownloadVersion = "v1.6.1-server-1.31.0-151.0",
ExtraArgs = new List<string>
{
// Disable search attribute cache
Expand Down
Loading