From 1a03e937dcd1a9a0ab1fee6be0f458291a3315af Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Fri, 27 Mar 2026 15:49:51 -0500 Subject: [PATCH 1/4] Add getVersion failure regression test --- .../WorkflowFailureGetVersionTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/failure/WorkflowFailureGetVersionTest.java diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/failure/WorkflowFailureGetVersionTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/failure/WorkflowFailureGetVersionTest.java new file mode 100644 index 0000000000..d240c5d86f --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/failure/WorkflowFailureGetVersionTest.java @@ -0,0 +1,81 @@ +package io.temporal.workflow.failure; + +import static io.temporal.testUtils.Eventually.assertEventually; + +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.history.v1.HistoryEvent; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowException; +import io.temporal.client.WorkflowStub; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.shared.TestWorkflows.TestWorkflow1; +import java.time.Duration; +import java.util.List; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +public class WorkflowFailureGetVersionTest { + + @Rule public TestName testName = new TestName(); + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestWorkflowGetVersionAndException.class) + .build(); + + @Test + public void getVersionAndException() { + TestWorkflow1 workflow = testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class); + WorkflowExecution execution = WorkflowClient.start(workflow::execute, testName.getMethodName()); + WorkflowStub workflowStub = WorkflowStub.fromTyped(workflow); + + try { + HistoryEvent workflowTaskFailed = + assertEventually( + Duration.ofSeconds(5), + () -> { + List failedEvents = + testWorkflowRule.getHistoryEvents( + execution.getWorkflowId(), EventType.EVENT_TYPE_WORKFLOW_TASK_FAILED); + Assert.assertFalse("No workflow task failure recorded", failedEvents.isEmpty()); + return failedEvents.get(0); + }); + + Failure failure = + getDeepestFailure(workflowTaskFailed.getWorkflowTaskFailedEventAttributes().getFailure()); + Assert.assertEquals("Any error", failure.getMessage()); + Assert.assertTrue(failure.hasApplicationFailureInfo()); + Assert.assertEquals( + RuntimeException.class.getName(), failure.getApplicationFailureInfo().getType()); + } finally { + try { + workflowStub.terminate("terminate test workflow"); + } catch (WorkflowException ignored) { + } + } + } + + private static Failure getDeepestFailure(Failure failure) { + while (failure.hasCause()) { + failure = failure.getCause(); + } + return failure; + } + + public static class TestWorkflowGetVersionAndException implements TestWorkflow1 { + + @Override + public String execute(String unused) { + String changeId = "change-id"; + Workflow.getVersion(changeId, Workflow.DEFAULT_VERSION, 1); + Workflow.getVersion(changeId, Workflow.DEFAULT_VERSION, 1); + throw new RuntimeException("Any error"); + } + } +} From d71f645e1f0c13756282750f060cb8bcca879bc6 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Fri, 27 Mar 2026 16:07:23 -0500 Subject: [PATCH 2/4] Add SKIP_YIELD_ON_VERSION to WorkflowStateMachines.initialFlags, fixing exception masking after multiple `getVersion()` calls --- .../temporal/internal/statemachines/WorkflowStateMachines.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java index 2f2c716f24..1dd15baacc 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java @@ -52,7 +52,8 @@ enum HandleEventStatus { /** Initial set of SDK flags that will be set on all new workflow executions. */ @VisibleForTesting public static List initialFlags = - Collections.unmodifiableList(Arrays.asList(SdkFlag.SKIP_YIELD_ON_DEFAULT_VERSION)); + Collections.unmodifiableList( + Arrays.asList(SdkFlag.SKIP_YIELD_ON_DEFAULT_VERSION, SdkFlag.SKIP_YIELD_ON_VERSION)); /** * Keep track of the change versions that have been seen by the SDK. This is used to generate the From 5b4cc7e64aada45d73bbc6905814a4573392946f Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Mon, 30 Mar 2026 16:09:57 -0500 Subject: [PATCH 3/4] default to `SKIP_YIELD_ON_VERSION` using `tryUseSdkFlag()` instead of adding it to `initialFlags` --- .../temporal/internal/statemachines/WorkflowStateMachines.java | 3 +-- .../java/io/temporal/internal/sync/SyncWorkflowContext.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java index 1dd15baacc..cb2b507d7a 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java @@ -52,8 +52,7 @@ enum HandleEventStatus { /** Initial set of SDK flags that will be set on all new workflow executions. */ @VisibleForTesting public static List initialFlags = - Collections.unmodifiableList( - Arrays.asList(SdkFlag.SKIP_YIELD_ON_DEFAULT_VERSION, SdkFlag.SKIP_YIELD_ON_VERSION)); + Collections.singletonList(SdkFlag.SKIP_YIELD_ON_DEFAULT_VERSION); /** * Keep track of the change versions that have been seen by the SDK. This is used to generate the 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 7a956dc4b7..531acb12c3 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 @@ -1174,7 +1174,7 @@ public int getVersion(String changeId, int minSupported, int maxSupported) { * Previously the SDK would yield on the getVersion call to the scheduler. This is not ideal because it can lead to non-deterministic * scheduling if the getVersion call was removed. * */ - if (replayContext.checkSdkFlag(SdkFlag.SKIP_YIELD_ON_VERSION)) { + if (replayContext.tryUseSdkFlag(SdkFlag.SKIP_YIELD_ON_VERSION)) { // This can happen if we are replaying a workflow and encounter a getVersion call that did not // exist on the original execution and the range does not include the default version. if (versionToUse == null) { From 7f6ff9772dfb44022b63857678e6ed74b1c1eefd Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Mon, 30 Mar 2026 16:34:41 -0500 Subject: [PATCH 4/4] Strengthen GetVersionMultithreadingRemoveTest to verify that setting `SKIP_YIELD_ON_VERSION` by default corrects for the issue. --- .../GetVersionMultithreadingRemoveTest.java | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/versionTests/GetVersionMultithreadingRemoveTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/versionTests/GetVersionMultithreadingRemoveTest.java index cc49e88002..13f9753321 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/versionTests/GetVersionMultithreadingRemoveTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/versionTests/GetVersionMultithreadingRemoveTest.java @@ -1,12 +1,13 @@ package io.temporal.workflow.versionTests; -import static org.junit.Assert.*; -import static org.junit.Assume.assumeTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import io.temporal.internal.Issue; import io.temporal.testing.WorkflowReplayer; import io.temporal.testing.internal.SDKTestOptions; import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.testing.internal.TracingWorkerInterceptor; import io.temporal.worker.WorkerOptions; import io.temporal.workflow.Async; import io.temporal.workflow.Workflow; @@ -14,19 +15,19 @@ import io.temporal.workflow.shared.TestWorkflows; import io.temporal.workflow.unsafe.WorkflowUnsafe; import java.time.Duration; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; @Issue("https://github.com/temporalio/sdk-java/issues/2307") -public class GetVersionMultithreadingRemoveTest extends BaseVersionTest { +public class GetVersionMultithreadingRemoveTest { private static boolean hasReplayed; @Rule public SDKTestWorkflowRule testWorkflowRule = SDKTestWorkflowRule.newBuilder() - .setWorkflowTypes( - getDefaultWorkflowImplementationOptions(), TestGetVersionWorkflowImpl.class) + .setWorkflowTypes(TestGetVersionWorkflowImpl.class) .setActivityImplementations(new TestActivities.TestActivitiesImpl()) // Forcing a replay. Full history arrived from a normal queue causing a replay. .setWorkerOptions( @@ -35,18 +36,30 @@ public class GetVersionMultithreadingRemoveTest extends BaseVersionTest { .build()) .build(); - public GetVersionMultithreadingRemoveTest(boolean setVersioningFlag, boolean upsertVersioningSA) { - super(setVersioningFlag, upsertVersioningSA); + @Before + public void setUp() { + hasReplayed = false; } @Test public void testGetVersionMultithreadingRemoval() { - assumeTrue("This test only passes if SKIP_YIELD_ON_VERSION is enabled", setVersioningFlag); TestWorkflows.TestWorkflow1 workflowStub = testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + assertTrue(hasReplayed); assertEquals("activity1", result); + testWorkflowRule + .getInterceptor(TracingWorkerInterceptor.class) + .setExpected( + "interceptExecuteWorkflow " + SDKTestWorkflowRule.UUID_REGEXP, + "newThread workflow-method", + "newThread null", + "getVersion", + "executeActivity customActivity1", + "sleep PT1S", + "activity customActivity1"); } @Test @@ -76,9 +89,8 @@ public String execute(String taskQueue) { } else { hasReplayed = true; } - String result = - "activity" + testActivities.activity1(1); // This is executed in non-replay mode. - return result; + + return "activity" + testActivities.activity1(1); } } }