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 a1d92fc97..cdf4818bb 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 c03e5e4e0..5386caef8 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 = 3; + @Rule public SDKTestWorkflowRule testWorkflowRule = SDKTestWorkflowRule.newBuilder().setDoNotStart(true).build(); @@ -96,6 +100,35 @@ public void maxAttemptDecreasedOnRetryWakeUp() { controlledActivity.verifyAttempts(); } + @Test + public void repeatedTimeoutsDoNotBuildAnUnboundedFailureChain() { + Worker worker = testWorkflowRule.getWorker(); + ControlledActivityImpl activity = + new ControlledActivityImpl( + Collections.singletonList(ControlledActivityImpl.Outcome.SLEEP), TIMEOUT_ATTEMPTS, 1); + 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(); + } + } }