From 45fa4fa40270cb1414ac0ec90829c403e0034160 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Wed, 12 Aug 2026 22:36:41 -0700 Subject: [PATCH 1/2] Fix unbounded timeout failure chain --- .../internal/sync/SyncWorkflowContext.java | 3 +- ...ityRetryOverLocalBackoffThresholdTest.java | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java index a1d92fc971..cdf4818bb9 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java @@ -502,7 +502,8 @@ public void executeLocalActivityOverLocalRetryThreshold( input, originalScheduledTime, laException.getLastAttempt() + 1, - laException.getFailure(), + // Carry the attempt failure, not the local ActivityFailure wrapper. + laException.getFailure().getCause(), result); return null; }); diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java index c03e5e4e01..7c2702dddb 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java @@ -6,11 +6,13 @@ import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.enums.v1.EventType; import io.temporal.api.enums.v1.RetryState; +import io.temporal.api.enums.v1.TimeoutType; import io.temporal.api.history.v1.HistoryEvent; import io.temporal.client.WorkflowException; import io.temporal.client.WorkflowStub; import io.temporal.common.RetryOptions; import io.temporal.failure.ActivityFailure; +import io.temporal.failure.TimeoutFailure; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.worker.Worker; import io.temporal.workflow.Workflow; @@ -26,6 +28,8 @@ public class LocalActivityRetryOverLocalBackoffThresholdTest { + private static final int TIMEOUT_ATTEMPTS = 53; + @Rule public SDKTestWorkflowRule testWorkflowRule = SDKTestWorkflowRule.newBuilder().setDoNotStart(true).build(); @@ -96,6 +100,35 @@ public void maxAttemptDecreasedOnRetryWakeUp() { controlledActivity.verifyAttempts(); } + @Test(timeout = 120_000) + public void repeatedTimeoutsDoNotBuildAnUnboundedFailureChain() { + Worker worker = testWorkflowRule.getWorker(); + ControlledActivityImpl activity = + new ControlledActivityImpl( + Collections.singletonList(ControlledActivityImpl.Outcome.SLEEP), TIMEOUT_ATTEMPTS, 100); + worker.registerActivitiesImplementations(activity); + worker.registerWorkflowImplementationTypes(TimingOutWorkflowImpl.class); + testWorkflowRule.getTestEnvironment().start(); + + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + + WorkflowException e = + assertThrows( + WorkflowException.class, () -> workflowStub.execute(testWorkflowRule.getTaskQueue())); + assertTrue(e.getCause() instanceof ActivityFailure); + ActivityFailure activityFailure = (ActivityFailure) e.getCause(); + assertEquals(RetryState.RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED, activityFailure.getRetryState()); + assertTrue(activityFailure.getCause() instanceof TimeoutFailure); + TimeoutFailure timeoutFailure = (TimeoutFailure) activityFailure.getCause(); + assertEquals(TimeoutType.TIMEOUT_TYPE_START_TO_CLOSE, timeoutFailure.getTimeoutType()); + assertTrue(timeoutFailure.getCause() instanceof TimeoutFailure); + TimeoutFailure previousTimeoutFailure = (TimeoutFailure) timeoutFailure.getCause(); + assertEquals(TimeoutType.TIMEOUT_TYPE_START_TO_CLOSE, previousTimeoutFailure.getTimeoutType()); + assertNull(previousTimeoutFailure.getCause()); + activity.verifyAttempts(); + } + public static class TestWorkflowImpl implements TestWorkflows.TestWorkflow1 { @Override @@ -144,4 +177,25 @@ public String execute(String taskQueue) { return "ignored"; } } + + public static class TimingOutWorkflowImpl implements TestWorkflows.TestWorkflow1 { + + @Override + public String execute(String taskQueue) { + LocalActivityOptions options = + LocalActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofMillis(10)) + .setLocalRetryThreshold(Duration.ofMillis(1)) + .setRetryOptions( + RetryOptions.newBuilder() + .setInitialInterval(Duration.ofMillis(2)) + .setBackoffCoefficient(1) + .setMaximumAttempts(TIMEOUT_ATTEMPTS) + .build()) + .build(); + TestActivities.NoArgsReturnsStringActivity activity = + Workflow.newLocalActivityStub(TestActivities.NoArgsReturnsStringActivity.class, options); + return activity.execute(); + } + } } From df1786ea1466d71971431084471367493dbc1684 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Thu, 13 Aug 2026 08:24:44 -0700 Subject: [PATCH 2/2] Make test faster --- .../LocalActivityRetryOverLocalBackoffThresholdTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java index 7c2702dddb..5386caef88 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/LocalActivityRetryOverLocalBackoffThresholdTest.java @@ -28,7 +28,7 @@ public class LocalActivityRetryOverLocalBackoffThresholdTest { - private static final int TIMEOUT_ATTEMPTS = 53; + private static final int TIMEOUT_ATTEMPTS = 3; @Rule public SDKTestWorkflowRule testWorkflowRule = @@ -100,12 +100,12 @@ public void maxAttemptDecreasedOnRetryWakeUp() { controlledActivity.verifyAttempts(); } - @Test(timeout = 120_000) + @Test public void repeatedTimeoutsDoNotBuildAnUnboundedFailureChain() { Worker worker = testWorkflowRule.getWorker(); ControlledActivityImpl activity = new ControlledActivityImpl( - Collections.singletonList(ControlledActivityImpl.Outcome.SLEEP), TIMEOUT_ATTEMPTS, 100); + Collections.singletonList(ControlledActivityImpl.Outcome.SLEEP), TIMEOUT_ATTEMPTS, 1); worker.registerActivitiesImplementations(activity); worker.registerWorkflowImplementationTypes(TimingOutWorkflowImpl.class); testWorkflowRule.getTestEnvironment().start();