From 941a05616756310e1e4f01e525dd234bffa7f1ee Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Thu, 15 Jan 2026 09:24:36 -0800 Subject: [PATCH 1/5] Add schedule-to-start and start-to-close to nexus operations --- src/Temporalio/Worker/WorkflowInstance.cs | 8 +++++++ .../Workflows/NexusOperationOptions.cs | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Temporalio/Worker/WorkflowInstance.cs b/src/Temporalio/Worker/WorkflowInstance.cs index 16fa4aca..129e0564 100644 --- a/src/Temporalio/Worker/WorkflowInstance.cs +++ b/src/Temporalio/Worker/WorkflowInstance.cs @@ -2572,6 +2572,14 @@ public override Task> StartNexusOperationAsync headers) { cmd.NexusHeader.Add(headers); diff --git a/src/Temporalio/Workflows/NexusOperationOptions.cs b/src/Temporalio/Workflows/NexusOperationOptions.cs index eda156c0..10a316d2 100644 --- a/src/Temporalio/Workflows/NexusOperationOptions.cs +++ b/src/Temporalio/Workflows/NexusOperationOptions.cs @@ -11,9 +11,30 @@ public class NexusOperationOptions : ICloneable { /// /// Gets or sets the schedule to close timeout. + /// Indicates how long the caller is willing to wait for operation completion. + /// Calls are retried internally by the server. /// public TimeSpan? ScheduleToCloseTimeout { get; set; } + /// + /// Gets or sets the schedule to start timeout. + /// 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. + /// + public TimeSpan? ScheduleToStartTimeout { get; set; } + + /// + /// Gets or sets the start to close timeout. + /// 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. + /// + public TimeSpan? StartToCloseTimeout { get; set; } + /// /// Gets or sets the summary. /// From 815031c1282f49f6af3eb56a19395cc26b73d73a Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Thu, 19 Feb 2026 22:01:53 -0800 Subject: [PATCH 2/5] Add test --- .../Worker/NexusWorkerTests.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs b/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs index 16c02b64..7f0c1c31 100644 --- a/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs +++ b/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs @@ -299,6 +299,9 @@ await Workflow.CreateNexusClient(endpoint). })); Assert.IsType( Assert.IsType(exc.InnerException).InnerException); + var timeoutExc = Assert.IsType( + Assert.IsType(exc.InnerException).InnerException); + Assert.Equal(TimeoutType.ScheduleToClose, 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))); @@ -338,6 +341,75 @@ await Workflow.CreateNexusClient(endpoint). Assert.Equal(expectedSummary, actualSummary); } + [Fact] + public async Task ExecuteNexusOperationAsync_ScheduleToStartTimeout_FailsAsExpected() + { + var contextSource = new TaskCompletionSource(); + var workerOptions = new TemporalWorkerOptions($"tq-{Guid.NewGuid()}"). + AddNexusService(new HandlerFactoryStringService(() => + OperationHandler.Sync(async (ctx, name) => + { + contextSource.SetResult(ctx); + try + { + await Task.Delay(4000, 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(() => + RunInWorkflowAsync(workerOptions, async () => + { + await Workflow.CreateNexusClient(endpoint). + ExecuteNexusOperationAsync( + svc => svc.DoSomething("some-name"), + new() { ScheduleToStartTimeout = TimeSpan.FromSeconds(1) }); + })); + var timeoutExc = Assert.IsType( + Assert.IsType(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(); + var endpoint = await CreateNexusEndpointAsync(workerOptions.TaskQueue!); + // Confirm the workflow fails with the timeout + var exc = await Assert.ThrowsAsync(() => + RunInWorkflowAsync(workerOptions, async () => + { + await Workflow.CreateNexusClient(endpoint). + ExecuteNexusOperationAsync( + svc => svc.DoSomething("some-name"), + new() + { + ScheduleToStartTimeout = TimeSpan.FromSeconds(30), + StartToCloseTimeout = TimeSpan.FromSeconds(2), + }); + })); + var timeoutExc = Assert.IsType( + Assert.IsType(exc.InnerException).InnerException); + Assert.Equal(TimeoutType.StartToClose, timeoutExc.TimeoutType); + } + [Workflow] public class WaitForSignalWorkflow { From aff8eb2538f0d44ef824ec2914ce88c533c1272c Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Thu, 19 Feb 2026 23:41:21 -0800 Subject: [PATCH 3/5] Use correct dev server --- tests/Temporalio.Tests/Worker/NexusWorkerTests.cs | 6 ++---- tests/Temporalio.Tests/WorkflowEnvironment.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs b/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs index 7f0c1c31..3c9a316c 100644 --- a/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs +++ b/tests/Temporalio.Tests/Worker/NexusWorkerTests.cs @@ -297,8 +297,6 @@ await Workflow.CreateNexusClient(endpoint). svc => svc.DoSomething("some-name"), new() { ScheduleToCloseTimeout = TimeSpan.FromSeconds(2) }); })); - Assert.IsType( - Assert.IsType(exc.InnerException).InnerException); var timeoutExc = Assert.IsType( Assert.IsType(exc.InnerException).InnerException); Assert.Equal(TimeoutType.ScheduleToClose, timeoutExc.TimeoutType); @@ -352,7 +350,7 @@ public async Task ExecuteNexusOperationAsync_ScheduleToStartTimeout_FailsAsExpec contextSource.SetResult(ctx); try { - await Task.Delay(4000, ctx.CancellationToken); + await Task.Delay(40000, ctx.CancellationToken); return "done"; } catch (TaskCanceledException) @@ -368,7 +366,7 @@ public async Task ExecuteNexusOperationAsync_ScheduleToStartTimeout_FailsAsExpec await Workflow.CreateNexusClient(endpoint). ExecuteNexusOperationAsync( svc => svc.DoSomething("some-name"), - new() { ScheduleToStartTimeout = TimeSpan.FromSeconds(1) }); + new() { ScheduleToStartTimeout = TimeSpan.FromSeconds(2) }); })); var timeoutExc = Assert.IsType( Assert.IsType(exc.InnerException).InnerException); diff --git a/tests/Temporalio.Tests/WorkflowEnvironment.cs b/tests/Temporalio.Tests/WorkflowEnvironment.cs index b348d04c..cb058496 100644 --- a/tests/Temporalio.Tests/WorkflowEnvironment.cs +++ b/tests/Temporalio.Tests/WorkflowEnvironment.cs @@ -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 { // Disable search attribute cache From 122599c0716b986ad856796b09d7f2b458c2d2d4 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Fri, 20 Feb 2026 10:45:46 -0800 Subject: [PATCH 4/5] Add 's --- src/Temporalio/Workflows/NexusOperationOptions.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Temporalio/Workflows/NexusOperationOptions.cs b/src/Temporalio/Workflows/NexusOperationOptions.cs index 10a316d2..3dbfcd50 100644 --- a/src/Temporalio/Workflows/NexusOperationOptions.cs +++ b/src/Temporalio/Workflows/NexusOperationOptions.cs @@ -11,27 +11,27 @@ public class NexusOperationOptions : ICloneable { /// /// Gets or sets the schedule to close timeout. - /// Indicates how long the caller is willing to wait for operation completion. - /// Calls are retried internally by the server. + /// Indicates how long the caller is willing to wait for operation completion. + /// Calls are retried internally by the server. /// public TimeSpan? ScheduleToCloseTimeout { get; set; } /// /// Gets or sets the schedule to start timeout. - /// Indicates how long the caller is willing to wait for the operation to be started (or completed if synchronous) + /// 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. + /// Requires server version 1.31.0 or later. /// public TimeSpan? ScheduleToStartTimeout { get; set; } /// /// Gets or sets the start to close timeout. - /// Indicates how long the caller is willing to wait for an asynchronous operation to complete after it has been + /// 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. + /// Requires server version 1.31.0 or later. /// public TimeSpan? StartToCloseTimeout { get; set; } From 14b1270342458e7baa531dab03a8027be86ce8be Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Fri, 20 Feb 2026 11:01:29 -0800 Subject: [PATCH 5/5] Fix remarks --- src/Temporalio/Workflows/NexusOperationOptions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Temporalio/Workflows/NexusOperationOptions.cs b/src/Temporalio/Workflows/NexusOperationOptions.cs index 3dbfcd50..82e07972 100644 --- a/src/Temporalio/Workflows/NexusOperationOptions.cs +++ b/src/Temporalio/Workflows/NexusOperationOptions.cs @@ -11,28 +11,28 @@ public class NexusOperationOptions : ICloneable { /// /// Gets or sets the schedule to close timeout. + /// /// Indicates how long the caller is willing to wait for operation completion. /// Calls are retried internally by the server. - /// public TimeSpan? ScheduleToCloseTimeout { get; set; } /// /// Gets or sets the schedule to start timeout. + /// /// 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. - /// public TimeSpan? ScheduleToStartTimeout { get; set; } /// /// Gets or sets the start to close timeout. + /// /// 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. - /// public TimeSpan? StartToCloseTimeout { get; set; } ///