From ea83af1410d1152d92264b1761aa661b65ebc4ca Mon Sep 17 00:00:00 2001 From: Anshuman Kishore Pandey Date: Wed, 25 Jun 2025 13:24:14 +0200 Subject: [PATCH 1/2] Port go-client changes to support the enhanced versioning feature --- .../internal/replay/ClockDecisionContext.java | 28 ++- .../internal/replay/DecisionContext.java | 12 ++ .../internal/replay/DecisionContextImpl.java | 13 ++ .../internal/sync/SyncDecisionContext.java | 5 + .../internal/sync/WorkflowInternal.java | 13 ++ .../cadence/workflow/GetVersionOptions.java | 108 ++++++++++++ .../com/uber/cadence/workflow/Workflow.java | 67 ++++++- .../cadence/workflow/WorkflowInterceptor.java | 11 ++ .../workflow/WorkflowInterceptorBase.java | 5 + .../workflow/EnhancedGetVersionTest.java | 166 ++++++++++++++++++ .../workflow/GetVersionOptionsTest.java | 98 +++++++++++ 11 files changed, 519 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/uber/cadence/workflow/GetVersionOptions.java create mode 100644 src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java create mode 100644 src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java diff --git a/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java b/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java index 339029edb..e8901ce43 100644 --- a/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java +++ b/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java @@ -320,6 +320,11 @@ private void handleVersionMarker(MarkerRecordedEventAttributes attributes) { GetVersionResult getVersion( String changeId, DataConverter converter, int minSupported, int maxSupported) { + return getVersion(changeId, converter, minSupported, maxSupported, GetVersionOptions.newBuilder().build()); + } + + GetVersionResult getVersion( + String changeId, DataConverter converter, int minSupported, int maxSupported, GetVersionOptions options) { Predicate changeIdEquals = (attributes) -> { MarkerHandler.MarkerInterface markerData = @@ -328,6 +333,9 @@ GetVersionResult getVersion( }; decisions.addAllMissingVersionMarker(true, Optional.of(changeIdEquals)); + // Determine which version to use based on options + int versionToUse = determineVersionToUse(minSupported, maxSupported, options); + final MarkerHandler.HandleResult result = versionHandler.handle( changeId, @@ -336,14 +344,14 @@ GetVersionResult getVersion( if (stored.isPresent()) { return Optional.empty(); } - return Optional.of(converter.toData(maxSupported)); + return Optional.of(converter.toData(versionToUse)); }); final boolean isNewlyAdded = result.isNewlyStored(); Map searchAttributesForChangeVersion = null; if (isNewlyAdded) { searchAttributesForChangeVersion = - createSearchAttributesForChangeVersion(changeId, maxSupported, versionMap); + createSearchAttributesForChangeVersion(changeId, versionToUse, versionMap); } Integer version = versionMap.get(changeId); @@ -361,6 +369,22 @@ GetVersionResult getVersion( return new GetVersionResult(version, isNewlyAdded, searchAttributesForChangeVersion); } + private int determineVersionToUse(int minSupported, int maxSupported, GetVersionOptions options) { + if (isReplaying()) { + return WorkflowInternal.DEFAULT_VERSION; + } + + if (options.getCustomVersion().isPresent()) { + return options.getCustomVersion().get(); + } + + if (options.isUseMinVersion()) { + return minSupported; + } + + return maxSupported; + } + private void validateVersion(String changeID, int version, int minSupported, int maxSupported) { if ((version < minSupported || version > maxSupported) && version != WorkflowInternal.DEFAULT_VERSION) { diff --git a/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java b/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java index e3e76c8e9..37c9a342b 100644 --- a/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java +++ b/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java @@ -194,6 +194,18 @@ Optional mutableSideEffect( */ int getVersion(String changeID, DataConverter dataConverter, int minSupported, int maxSupported); + /** + * Enhanced version of getVersion with additional options for version control. + * + * @param changeID identifier of a particular change + * @param dataConverter data converter for serialization + * @param minSupported min version supported for the change + * @param maxSupported max version supported for the change + * @param options version control options + * @return version + */ + int getVersion(String changeID, DataConverter dataConverter, int minSupported, int maxSupported, GetVersionOptions options); + Random newRandom(); /** @return scope to be used for metrics reporting. */ diff --git a/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java b/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java index c3bc89a50..923fd937d 100644 --- a/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java +++ b/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java @@ -302,6 +302,19 @@ public int getVersion( return results.getVersion(); } + @Override + public int getVersion( + String changeID, DataConverter converter, int minSupported, int maxSupported, GetVersionOptions options) { + final ClockDecisionContext.GetVersionResult results = + workflowClock.getVersion(changeID, converter, minSupported, maxSupported, options); + if (results.shouldUpdateCadenceChangeVersion()) { + upsertSearchAttributes( + InternalUtils.convertMapToSearchAttributes( + results.getSearchAttributesForChangeVersion())); + } + return results.getVersion(); + } + @Override public long currentTimeMillis() { return workflowClock.currentTimeMillis(); diff --git a/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java b/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java index c204ff22a..b65177002 100644 --- a/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java +++ b/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java @@ -620,6 +620,11 @@ public int getVersion(String changeID, int minSupported, int maxSupported) { return context.getVersion(changeID, converter, minSupported, maxSupported); } + @Override + public int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + return context.getVersion(changeID, converter, minSupported, maxSupported, options); + } + void fireTimers() { timers.fireTimers(context.currentTimeMillis()); } diff --git a/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java b/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java index 217195841..92b57afa2 100644 --- a/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java +++ b/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java @@ -253,6 +253,19 @@ public static int getVersion(String changeID, int minSupported, int maxSupported return getWorkflowInterceptor().getVersion(changeID, minSupported, maxSupported); } + /** + * Enhanced version of getVersion with additional options for version control. + * + * @param changeID identifier of a particular change + * @param minSupported min version supported for the change + * @param maxSupported max version supported for the change + * @param options version control options + * @return version + */ + public static int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + return getWorkflowInterceptor().getVersion(changeID, minSupported, maxSupported, options); + } + public static Promise> promiseAllOf(Collection> promises) { return new AllOfPromise<>(promises); } diff --git a/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java b/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java new file mode 100644 index 000000000..777865826 --- /dev/null +++ b/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.uber.cadence.workflow; + +import java.util.Optional; + +/** + * Options for configuring GetVersion behavior. + * This class provides a builder pattern for configuring version control options. + * + *

Example usage: + *


+ * // Force a specific version
+ * GetVersionOptions options = GetVersionOptions.newBuilder()
+ *     .executeWithVersion(2)
+ *     .build();
+ * 
+ * // Use minimum supported version
+ * GetVersionOptions options = GetVersionOptions.newBuilder()
+ *     .executeWithMinVersion()
+ *     .build();
+ * 
+ */ +public final class GetVersionOptions { + private final Optional customVersion; + private final boolean useMinVersion; + + private GetVersionOptions(Optional customVersion, boolean useMinVersion) { + this.customVersion = customVersion; + this.useMinVersion = useMinVersion; + } + + /** + * Returns the custom version if specified, otherwise empty. + */ + public Optional getCustomVersion() { + return customVersion; + } + + /** + * Returns true if the minimum version should be used instead of maximum version. + */ + public boolean isUseMinVersion() { + return useMinVersion; + } + + /** + * Creates a new builder for GetVersionOptions. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder for GetVersionOptions. + */ + public static class Builder { + private Optional customVersion = Optional.empty(); + private boolean useMinVersion = false; + + /** + * Forces a specific version to be returned when executed for the first time, + * instead of returning maxSupported version. + * + * @param version the specific version to use + * @return this builder + */ + public Builder executeWithVersion(int version) { + this.customVersion = Optional.of(version); + return this; + } + + /** + * Makes GetVersion return minSupported version when executed for the first time, + * instead of returning maxSupported version. + * + * @return this builder + */ + public Builder executeWithMinVersion() { + this.useMinVersion = true; + return this; + } + + /** + * Builds the GetVersionOptions instance. + * + * @return the configured GetVersionOptions + */ + public GetVersionOptions build() { + return new GetVersionOptions(customVersion, useMinVersion); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/uber/cadence/workflow/Workflow.java b/src/main/java/com/uber/cadence/workflow/Workflow.java index b5c3966ae..0f219536c 100644 --- a/src/main/java/com/uber/cadence/workflow/Workflow.java +++ b/src/main/java/com/uber/cadence/workflow/Workflow.java @@ -353,7 +353,7 @@ * executed in parallel. *
  • Do not call any non deterministic functions like non seeded random or {@link * UUID#randomUUID()} directly form the workflow code. Always do this in activities. - *
  • Don’t perform any IO or service calls as they are not usually deterministic. Use activities + *
  • Don't perform any IO or service calls as they are not usually deterministic. Use activities * for this. *
  • Only use {@link #currentTimeMillis()} to get the current time inside a workflow. *
  • Do not use native Java {@link Thread} or any other multi-threaded classes like {@link @@ -369,7 +369,7 @@ *
  • Use {@link WorkflowQueue} instead of {@link java.util.concurrent.BlockingQueue}. *
  • Don't change workflow code when there are open workflows. The ability to do updates through * visioning is TBD. - *
  • Don’t access configuration APIs directly from a workflow because changes in the + *
  • Don't access configuration APIs directly from a workflow because changes in the * configuration might affect a workflow execution path. Pass it as an argument to a workflow * function or use an activity to load it. * @@ -1123,15 +1123,15 @@ public static R mutableSideEffect( * * * The reason to keep it is: 1) it ensures that if there is older version execution still running, - * it will fail here and not proceed; 2) if you ever need to make more changes for “fooChange”, + * it will fail here and not proceed; 2) if you ever need to make more changes for "fooChange", * for example change activity3 to activity4, you just need to update the maxVersion from 2 to 3. * *

    Note that, you only need to preserve the first call to GetVersion() for each changeID. All * subsequent call to GetVersion() with same changeID are safe to remove. However, if you really * want to get rid of the first GetVersion() call as well, you can do so, but you need to make - * sure: 1) all older version executions are completed; 2) you can no longer use “fooChange” as + * sure: 1) all older version executions are completed; 2) you can no longer use "fooChange" as * changeID. If you ever need to make changes to that same part, you would need to use a different - * changeID like “fooChange-fix2”, and start minVersion from DefaultVersion again. + * changeID like "fooChange-fix2", and start minVersion from DefaultVersion again. * * @param changeID identifier of a particular change. All calls to getVersion that share a * changeID are guaranteed to return the same version number. Use this to perform multiple @@ -1144,6 +1144,63 @@ public static int getVersion(String changeID, int minSupported, int maxSupported return WorkflowInternal.getVersion(changeID, minSupported, maxSupported); } + /** + * Enhanced version of {@code getVersion} with additional options for version control. + * This method provides more granular control over version execution and enables safer deployment strategies. + * + *

    Example usage with custom version: + *

    
    +   * int version = Workflow.getVersion("changeId", 1, 3, 
    +   *     GetVersionOptions.newBuilder().executeWithVersion(2).build());
    +   * 
    + * + *

    Example usage with minimum version: + *

    
    +   * int version = Workflow.getVersion("changeId", 1, 3,
    +   *     GetVersionOptions.newBuilder().executeWithMinVersion().build());
    +   * 
    + * + * @param changeID identifier of a particular change + * @param minSupported min version supported for the change + * @param maxSupported max version supported for the change + * @param options version control options + * @return version + */ + public static int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + return WorkflowInternal.getVersion(changeID, minSupported, maxSupported, options); + } + + /** + * Convenience method that forces a specific version to be returned when executed for the first time. + * + * @param changeID identifier of a particular change + * @param minSupported min version supported for the change + * @param maxSupported max version supported for the change + * @param customVersion the specific version to use + * @return version + */ + public static int getVersionWithCustomVersion(String changeID, int minSupported, int maxSupported, int customVersion) { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(customVersion) + .build(); + return getVersion(changeID, minSupported, maxSupported, options); + } + + /** + * Convenience method that makes GetVersion return minSupported version when executed for the first time. + * + * @param changeID identifier of a particular change + * @param minSupported min version supported for the change + * @param maxSupported max version supported for the change + * @return version + */ + public static int getVersionWithMinVersion(String changeID, int minSupported, int maxSupported) { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithMinVersion() + .build(); + return getVersion(changeID, minSupported, maxSupported, options); + } + /** * Get scope for reporting business metrics in workflow logic. This should be used instead of * creating new metrics scopes as it is able to dedup metrics during replay. diff --git a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java index c744ae88f..9796e0426 100644 --- a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java +++ b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java @@ -127,6 +127,17 @@ R mutableSideEffect( int getVersion(String changeID, int minSupported, int maxSupported); + /** + * Enhanced version of getVersion with additional options for version control. + * + * @param changeID identifier of a particular change + * @param minSupported min version supported for the change + * @param maxSupported max version supported for the change + * @param options version control options + * @return version + */ + int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options); + void continueAsNew( Optional workflowType, Optional options, Object[] args); diff --git a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java index b6f436160..4aad904ab 100644 --- a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java +++ b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java @@ -134,6 +134,11 @@ public int getVersion(String changeID, int minSupported, int maxSupported) { return next.getVersion(changeID, minSupported, maxSupported); } + @Override + public int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + return next.getVersion(changeID, minSupported, maxSupported, options); + } + @Override public void continueAsNew( Optional workflowType, Optional options, Object[] args) { diff --git a/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java b/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java new file mode 100644 index 000000000..5de40d743 --- /dev/null +++ b/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.uber.cadence.workflow; + +import static org.junit.Assert.*; + +import com.uber.cadence.testing.TestWorkflowEnvironment; +import com.uber.cadence.worker.Worker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class EnhancedGetVersionTest { + + private TestWorkflowEnvironment testEnvironment; + private Worker worker; + + @Before + public void setUp() { + testEnvironment = TestWorkflowEnvironment.newInstance(); + worker = testEnvironment.newWorker("test-task-list"); + } + + @After + public void tearDown() { + testEnvironment.close(); + } + + @Test + public void testGetVersionWithCustomVersion() { + worker.registerWorkflowImplementationTypes(TestWorkflowWithCustomVersion.class); + testEnvironment.start(); + + TestWorkflowWithCustomVersion workflow = testEnvironment.newWorkflowStub(TestWorkflowWithCustomVersion.class); + String result = workflow.execute("test-input"); + + assertEquals("custom-version-result", result); + } + + @Test + public void testGetVersionWithMinVersion() { + worker.registerWorkflowImplementationTypes(TestWorkflowWithMinVersion.class); + testEnvironment.start(); + + TestWorkflowWithMinVersion workflow = testEnvironment.newWorkflowStub(TestWorkflowWithMinVersion.class); + String result = workflow.execute("test-input"); + + assertEquals("min-version-result", result); + } + + @Test + public void testGetVersionWithOptions() { + worker.registerWorkflowImplementationTypes(TestWorkflowWithOptions.class); + testEnvironment.start(); + + TestWorkflowWithOptions workflow = testEnvironment.newWorkflowStub(TestWorkflowWithOptions.class); + String result = workflow.execute("test-input"); + + assertEquals("options-result", result); + } + + @Test + public void testConvenienceMethods() { + worker.registerWorkflowImplementationTypes(TestWorkflowWithConvenienceMethods.class); + testEnvironment.start(); + + TestWorkflowWithConvenienceMethods workflow = testEnvironment.newWorkflowStub(TestWorkflowWithConvenienceMethods.class); + String result = workflow.execute("test-input"); + + assertEquals("convenience-result", result); + } + + public interface TestWorkflowWithCustomVersion { + @WorkflowMethod + String execute(String input); + } + + public static class TestWorkflowWithCustomVersionImpl implements TestWorkflowWithCustomVersion { + @Override + public String execute(String input) { + int version = Workflow.getVersion("test-change", 1, 3, + GetVersionOptions.newBuilder().executeWithVersion(2).build()); + + if (version == 2) { + return "custom-version-result"; + } else { + return "other-result"; + } + } + } + + public interface TestWorkflowWithMinVersion { + @WorkflowMethod + String execute(String input); + } + + public static class TestWorkflowWithMinVersionImpl implements TestWorkflowWithMinVersion { + @Override + public String execute(String input) { + int version = Workflow.getVersion("test-change", 1, 3, + GetVersionOptions.newBuilder().executeWithMinVersion().build()); + + if (version == 1) { + return "min-version-result"; + } else { + return "other-result"; + } + } + } + + public interface TestWorkflowWithOptions { + @WorkflowMethod + String execute(String input); + } + + public static class TestWorkflowWithOptionsImpl implements TestWorkflowWithOptions { + @Override + public String execute(String input) { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(2) + .build(); + + int version = Workflow.getVersion("test-change", 1, 3, options); + + if (version == 2) { + return "options-result"; + } else { + return "other-result"; + } + } + } + + public interface TestWorkflowWithConvenienceMethods { + @WorkflowMethod + String execute(String input); + } + + public static class TestWorkflowWithConvenienceMethodsImpl implements TestWorkflowWithConvenienceMethods { + @Override + public String execute(String input) { + int version1 = Workflow.getVersionWithCustomVersion("test-change-1", 1, 3, 2); + int version2 = Workflow.getVersionWithMinVersion("test-change-2", 1, 3); + + if (version1 == 2 && version2 == 1) { + return "convenience-result"; + } else { + return "other-result"; + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java b/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java new file mode 100644 index 000000000..c7277c107 --- /dev/null +++ b/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.uber.cadence.workflow; + +import static org.junit.Assert.*; + +import java.util.Optional; +import org.junit.Test; + +public class GetVersionOptionsTest { + + @Test + public void testExecuteWithVersion() { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(5) + .build(); + + assertEquals(Optional.of(5), options.getCustomVersion()); + assertFalse(options.isUseMinVersion()); + } + + @Test + public void testExecuteWithMinVersion() { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithMinVersion() + .build(); + + assertEquals(Optional.empty(), options.getCustomVersion()); + assertTrue(options.isUseMinVersion()); + } + + @Test + public void testDefaultOptions() { + GetVersionOptions options = GetVersionOptions.newBuilder().build(); + + assertEquals(Optional.empty(), options.getCustomVersion()); + assertFalse(options.isUseMinVersion()); + } + + @Test + public void testBuilderChaining() { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(3) + .executeWithMinVersion() + .build(); + + // When both are set, custom version takes precedence + assertEquals(Optional.of(3), options.getCustomVersion()); + assertTrue(options.isUseMinVersion()); + } + + @Test + public void testMultipleExecuteWithVersionCalls() { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(1) + .executeWithVersion(2) + .build(); + + // Last call should take precedence + assertEquals(Optional.of(2), options.getCustomVersion()); + assertFalse(options.isUseMinVersion()); + } + + @Test + public void testZeroVersion() { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(0) + .build(); + + assertEquals(Optional.of(0), options.getCustomVersion()); + assertFalse(options.isUseMinVersion()); + } + + @Test + public void testNegativeVersion() { + GetVersionOptions options = GetVersionOptions.newBuilder() + .executeWithVersion(-1) + .build(); + + assertEquals(Optional.of(-1), options.getCustomVersion()); + assertFalse(options.isUseMinVersion()); + } +} \ No newline at end of file From 82ac678c5028e55460e4778f064bae2fd5eec830 Mon Sep 17 00:00:00 2001 From: Anshuman Kishore Pandey Date: Wed, 25 Jun 2025 15:02:13 +0200 Subject: [PATCH 2/2] Port enhanced version control features from Go client Add support for ExecuteWithVersion and ExecuteWithMinVersion options to getVersion method, porting functionality from cadence-go-client PRs #1427 and #1428. Changes: - Add GetVersionOptions class with builder pattern for version control configuration - Add executeWithVersion() and executeWithMinVersion() methods to GetVersionOptions - Add overloaded getVersion() method that accepts GetVersionOptions parameter - Add convenience methods getVersionWithCustomVersion() and getVersionWithMinVersion() - Update all workflow interceptor interfaces and implementations to support new options - Update DecisionContext, ClockDecisionContext, and related classes for version selection logic - Add testing framework support for enhanced version control features - Add comprehensive test coverage for new functionality This enables safer deployment strategies and more granular control over workflow version execution, allowing developers to: - Force specific versions instead of max supported version - Use minimum supported version for backward compatibility - Test workflows with enhanced version control in test environments Fixes compilation errors and ensures all existing functionality remains compatible. --- .../internal/replay/ClockDecisionContext.java | 16 ++- .../internal/replay/DecisionContext.java | 8 +- .../internal/replay/DecisionContextImpl.java | 7 +- .../sync/DeterministicRunnerImpl.java | 11 ++ .../internal/sync/SyncDecisionContext.java | 4 +- .../sync/TestActivityEnvironmentInternal.java | 6 + .../internal/sync/WorkflowInternal.java | 6 +- .../cadence/workflow/GetVersionOptions.java | 111 +++++++-------- .../com/uber/cadence/workflow/Workflow.java | 29 ++-- .../cadence/workflow/WorkflowInterceptor.java | 2 +- .../workflow/WorkflowInterceptorBase.java | 3 +- ...TestWorkflowEnvironmentGetVersionTest.java | 134 ++++++++++++++++++ .../workflow/EnhancedGetVersionTest.java | 101 +++++++++---- .../workflow/GetVersionOptionsTest.java | 56 +++----- .../SignalWorkflowInterceptor.java | 6 + .../TracingWorkflowInterceptorFactory.java | 7 + 16 files changed, 359 insertions(+), 148 deletions(-) create mode 100644 src/test/java/com/uber/cadence/testing/TestWorkflowEnvironmentGetVersionTest.java diff --git a/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java b/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java index e8901ce43..ca90339c0 100644 --- a/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java +++ b/src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java @@ -33,6 +33,7 @@ import com.uber.cadence.workflow.ActivityFailureException; import com.uber.cadence.workflow.Functions.Func; import com.uber.cadence.workflow.Functions.Func1; +import com.uber.cadence.workflow.GetVersionOptions; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -320,11 +321,16 @@ private void handleVersionMarker(MarkerRecordedEventAttributes attributes) { GetVersionResult getVersion( String changeId, DataConverter converter, int minSupported, int maxSupported) { - return getVersion(changeId, converter, minSupported, maxSupported, GetVersionOptions.newBuilder().build()); + return getVersion( + changeId, converter, minSupported, maxSupported, GetVersionOptions.newBuilder().build()); } GetVersionResult getVersion( - String changeId, DataConverter converter, int minSupported, int maxSupported, GetVersionOptions options) { + String changeId, + DataConverter converter, + int minSupported, + int maxSupported, + GetVersionOptions options) { Predicate changeIdEquals = (attributes) -> { MarkerHandler.MarkerInterface markerData = @@ -373,15 +379,15 @@ private int determineVersionToUse(int minSupported, int maxSupported, GetVersion if (isReplaying()) { return WorkflowInternal.DEFAULT_VERSION; } - + if (options.getCustomVersion().isPresent()) { return options.getCustomVersion().get(); } - + if (options.isUseMinVersion()) { return minSupported; } - + return maxSupported; } diff --git a/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java b/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java index 37c9a342b..f154a8e87 100644 --- a/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java +++ b/src/main/java/com/uber/cadence/internal/replay/DecisionContext.java @@ -25,6 +25,7 @@ import com.uber.cadence.converter.DataConverter; import com.uber.cadence.workflow.Functions.Func; import com.uber.cadence.workflow.Functions.Func1; +import com.uber.cadence.workflow.GetVersionOptions; import com.uber.cadence.workflow.Promise; import com.uber.m3.tally.Scope; import java.time.Duration; @@ -204,7 +205,12 @@ Optional mutableSideEffect( * @param options version control options * @return version */ - int getVersion(String changeID, DataConverter dataConverter, int minSupported, int maxSupported, GetVersionOptions options); + int getVersion( + String changeID, + DataConverter dataConverter, + int minSupported, + int maxSupported, + GetVersionOptions options); Random newRandom(); diff --git a/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java b/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java index 923fd937d..b8d0e8469 100644 --- a/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java +++ b/src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java @@ -35,6 +35,7 @@ import com.uber.cadence.internal.worker.SingleWorkerOptions; import com.uber.cadence.workflow.Functions.Func; import com.uber.cadence.workflow.Functions.Func1; +import com.uber.cadence.workflow.GetVersionOptions; import com.uber.cadence.workflow.Promise; import com.uber.cadence.workflow.Workflow; import com.uber.m3.tally.Scope; @@ -304,7 +305,11 @@ public int getVersion( @Override public int getVersion( - String changeID, DataConverter converter, int minSupported, int maxSupported, GetVersionOptions options) { + String changeID, + DataConverter converter, + int minSupported, + int maxSupported, + GetVersionOptions options) { final ClockDecisionContext.GetVersionResult results = workflowClock.getVersion(changeID, converter, minSupported, maxSupported, options); if (results.shouldUpdateCadenceChangeVersion()) { diff --git a/src/main/java/com/uber/cadence/internal/sync/DeterministicRunnerImpl.java b/src/main/java/com/uber/cadence/internal/sync/DeterministicRunnerImpl.java index 84fb6a226..cc106eefe 100644 --- a/src/main/java/com/uber/cadence/internal/sync/DeterministicRunnerImpl.java +++ b/src/main/java/com/uber/cadence/internal/sync/DeterministicRunnerImpl.java @@ -36,6 +36,7 @@ import com.uber.cadence.internal.replay.StartChildWorkflowExecutionParameters; import com.uber.cadence.workflow.Functions.Func; import com.uber.cadence.workflow.Functions.Func1; +import com.uber.cadence.workflow.GetVersionOptions; import com.uber.cadence.workflow.Promise; import com.uber.m3.tally.Scope; import java.time.Duration; @@ -695,6 +696,16 @@ public int getVersion( throw new UnsupportedOperationException("not implemented"); } + @Override + public int getVersion( + String changeID, + DataConverter converter, + int minSupported, + int maxSupported, + GetVersionOptions options) { + throw new UnsupportedOperationException("not implemented"); + } + @Override public Random newRandom() { throw new UnsupportedOperationException("not implemented"); diff --git a/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java b/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java index b65177002..0bf55b9c0 100644 --- a/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java +++ b/src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java @@ -54,6 +54,7 @@ import com.uber.cadence.workflow.ContinueAsNewOptions; import com.uber.cadence.workflow.Functions; import com.uber.cadence.workflow.Functions.Func; +import com.uber.cadence.workflow.GetVersionOptions; import com.uber.cadence.workflow.Promise; import com.uber.cadence.workflow.SignalExternalWorkflowException; import com.uber.cadence.workflow.Workflow; @@ -621,7 +622,8 @@ public int getVersion(String changeID, int minSupported, int maxSupported) { } @Override - public int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + public int getVersion( + String changeID, int minSupported, int maxSupported, GetVersionOptions options) { return context.getVersion(changeID, converter, minSupported, maxSupported, options); } diff --git a/src/main/java/com/uber/cadence/internal/sync/TestActivityEnvironmentInternal.java b/src/main/java/com/uber/cadence/internal/sync/TestActivityEnvironmentInternal.java index 2ed9218b1..56ffb7694 100644 --- a/src/main/java/com/uber/cadence/internal/sync/TestActivityEnvironmentInternal.java +++ b/src/main/java/com/uber/cadence/internal/sync/TestActivityEnvironmentInternal.java @@ -242,6 +242,12 @@ public int getVersion(String changeID, int minSupported, int maxSupported) { throw new UnsupportedOperationException("not implemented"); } + @Override + public int getVersion( + String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + throw new UnsupportedOperationException("not implemented"); + } + @Override public void continueAsNew( Optional workflowType, Optional options, Object[] args) { diff --git a/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java b/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java index 92b57afa2..980fdb24c 100644 --- a/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java +++ b/src/main/java/com/uber/cadence/internal/sync/WorkflowInternal.java @@ -36,6 +36,7 @@ import com.uber.cadence.workflow.ExternalWorkflowStub; import com.uber.cadence.workflow.Functions; import com.uber.cadence.workflow.Functions.Func; +import com.uber.cadence.workflow.GetVersionOptions; import com.uber.cadence.workflow.Promise; import com.uber.cadence.workflow.QueryMethod; import com.uber.cadence.workflow.Workflow; @@ -255,14 +256,15 @@ public static int getVersion(String changeID, int minSupported, int maxSupported /** * Enhanced version of getVersion with additional options for version control. - * + * * @param changeID identifier of a particular change * @param minSupported min version supported for the change * @param maxSupported max version supported for the change * @param options version control options * @return version */ - public static int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) { + public static int getVersion( + String changeID, int minSupported, int maxSupported, GetVersionOptions options) { return getWorkflowInterceptor().getVersion(changeID, minSupported, maxSupported, options); } diff --git a/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java b/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java index 777865826..01ac1325e 100644 --- a/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java +++ b/src/main/java/com/uber/cadence/workflow/GetVersionOptions.java @@ -20,16 +20,17 @@ import java.util.Optional; /** - * Options for configuring GetVersion behavior. - * This class provides a builder pattern for configuring version control options. - * + * Options for configuring GetVersion behavior. This class provides a builder pattern for + * configuring version control options. + * *

    Example usage: + * *

    
      * // Force a specific version
      * GetVersionOptions options = GetVersionOptions.newBuilder()
      *     .executeWithVersion(2)
      *     .build();
    - * 
    + *
      * // Use minimum supported version
      * GetVersionOptions options = GetVersionOptions.newBuilder()
      *     .executeWithMinVersion()
    @@ -37,72 +38,64 @@
      * 
    */ public final class GetVersionOptions { - private final Optional customVersion; - private final boolean useMinVersion; + private final Optional customVersion; + private final boolean useMinVersion; - private GetVersionOptions(Optional customVersion, boolean useMinVersion) { - this.customVersion = customVersion; - this.useMinVersion = useMinVersion; - } + private GetVersionOptions(Optional customVersion, boolean useMinVersion) { + this.customVersion = customVersion; + this.useMinVersion = useMinVersion; + } - /** - * Returns the custom version if specified, otherwise empty. - */ - public Optional getCustomVersion() { - return customVersion; - } + /** Returns the custom version if specified, otherwise empty. */ + public Optional getCustomVersion() { + return customVersion; + } + + /** Returns true if the minimum version should be used instead of maximum version. */ + public boolean isUseMinVersion() { + return useMinVersion; + } + + /** Creates a new builder for GetVersionOptions. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** Builder for GetVersionOptions. */ + public static class Builder { + private Optional customVersion = Optional.empty(); + private boolean useMinVersion = false; /** - * Returns true if the minimum version should be used instead of maximum version. + * Forces a specific version to be returned when executed for the first time, instead of + * returning maxSupported version. + * + * @param version the specific version to use + * @return this builder */ - public boolean isUseMinVersion() { - return useMinVersion; + public Builder executeWithVersion(int version) { + this.customVersion = Optional.of(version); + return this; } /** - * Creates a new builder for GetVersionOptions. + * Makes GetVersion return minSupported version when executed for the first time, instead of + * returning maxSupported version. + * + * @return this builder */ - public static Builder newBuilder() { - return new Builder(); + public Builder executeWithMinVersion() { + this.useMinVersion = true; + return this; } /** - * Builder for GetVersionOptions. + * Builds the GetVersionOptions instance. + * + * @return the configured GetVersionOptions */ - public static class Builder { - private Optional customVersion = Optional.empty(); - private boolean useMinVersion = false; - - /** - * Forces a specific version to be returned when executed for the first time, - * instead of returning maxSupported version. - * - * @param version the specific version to use - * @return this builder - */ - public Builder executeWithVersion(int version) { - this.customVersion = Optional.of(version); - return this; - } - - /** - * Makes GetVersion return minSupported version when executed for the first time, - * instead of returning maxSupported version. - * - * @return this builder - */ - public Builder executeWithMinVersion() { - this.useMinVersion = true; - return this; - } - - /** - * Builds the GetVersionOptions instance. - * - * @return the configured GetVersionOptions - */ - public GetVersionOptions build() { - return new GetVersionOptions(customVersion, useMinVersion); - } + public GetVersionOptions build() { + return new GetVersionOptions(customVersion, useMinVersion); } -} \ No newline at end of file + } +} diff --git a/src/main/java/com/uber/cadence/workflow/Workflow.java b/src/main/java/com/uber/cadence/workflow/Workflow.java index 0f219536c..20f53864c 100644 --- a/src/main/java/com/uber/cadence/workflow/Workflow.java +++ b/src/main/java/com/uber/cadence/workflow/Workflow.java @@ -1145,16 +1145,18 @@ public static int getVersion(String changeID, int minSupported, int maxSupported } /** - * Enhanced version of {@code getVersion} with additional options for version control. - * This method provides more granular control over version execution and enables safer deployment strategies. + * Enhanced version of {@code getVersion} with additional options for version control. This method + * provides more granular control over version execution and enables safer deployment strategies. * *

    Example usage with custom version: + * *

    
    -   * int version = Workflow.getVersion("changeId", 1, 3, 
    +   * int version = Workflow.getVersion("changeId", 1, 3,
        *     GetVersionOptions.newBuilder().executeWithVersion(2).build());
        * 
    * *

    Example usage with minimum version: + * *

    
        * int version = Workflow.getVersion("changeId", 1, 3,
        *     GetVersionOptions.newBuilder().executeWithMinVersion().build());
    @@ -1166,12 +1168,14 @@ public static int getVersion(String changeID, int minSupported, int maxSupported
        * @param options version control options
        * @return version
        */
    -  public static int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) {
    +  public static int getVersion(
    +      String changeID, int minSupported, int maxSupported, GetVersionOptions options) {
         return WorkflowInternal.getVersion(changeID, minSupported, maxSupported, options);
       }
     
       /**
    -   * Convenience method that forces a specific version to be returned when executed for the first time.
    +   * Convenience method that forces a specific version to be returned when executed for the first
    +   * time.
        *
        * @param changeID identifier of a particular change
        * @param minSupported min version supported for the change
    @@ -1179,15 +1183,16 @@ public static int getVersion(String changeID, int minSupported, int maxSupported
        * @param customVersion the specific version to use
        * @return version
        */
    -  public static int getVersionWithCustomVersion(String changeID, int minSupported, int maxSupported, int customVersion) {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithVersion(customVersion)
    -        .build();
    +  public static int getVersionWithCustomVersion(
    +      String changeID, int minSupported, int maxSupported, int customVersion) {
    +    GetVersionOptions options =
    +        GetVersionOptions.newBuilder().executeWithVersion(customVersion).build();
         return getVersion(changeID, minSupported, maxSupported, options);
       }
     
       /**
    -   * Convenience method that makes GetVersion return minSupported version when executed for the first time.
    +   * Convenience method that makes GetVersion return minSupported version when executed for the
    +   * first time.
        *
        * @param changeID identifier of a particular change
        * @param minSupported min version supported for the change
    @@ -1195,9 +1200,7 @@ public static int getVersionWithCustomVersion(String changeID, int minSupported,
        * @return version
        */
       public static int getVersionWithMinVersion(String changeID, int minSupported, int maxSupported) {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithMinVersion()
    -        .build();
    +    GetVersionOptions options = GetVersionOptions.newBuilder().executeWithMinVersion().build();
         return getVersion(changeID, minSupported, maxSupported, options);
       }
     
    diff --git a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java
    index 9796e0426..055a9d2c9 100644
    --- a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java
    +++ b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptor.java
    @@ -129,7 +129,7 @@  R mutableSideEffect(
     
       /**
        * Enhanced version of getVersion with additional options for version control.
    -   * 
    +   *
        * @param changeID identifier of a particular change
        * @param minSupported min version supported for the change
        * @param maxSupported max version supported for the change
    diff --git a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java
    index 4aad904ab..ebaaffcdd 100644
    --- a/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java
    +++ b/src/main/java/com/uber/cadence/workflow/WorkflowInterceptorBase.java
    @@ -135,7 +135,8 @@ public int getVersion(String changeID, int minSupported, int maxSupported) {
       }
     
       @Override
    -  public int getVersion(String changeID, int minSupported, int maxSupported, GetVersionOptions options) {
    +  public int getVersion(
    +      String changeID, int minSupported, int maxSupported, GetVersionOptions options) {
         return next.getVersion(changeID, minSupported, maxSupported, options);
       }
     
    diff --git a/src/test/java/com/uber/cadence/testing/TestWorkflowEnvironmentGetVersionTest.java b/src/test/java/com/uber/cadence/testing/TestWorkflowEnvironmentGetVersionTest.java
    new file mode 100644
    index 000000000..aff628ff6
    --- /dev/null
    +++ b/src/test/java/com/uber/cadence/testing/TestWorkflowEnvironmentGetVersionTest.java
    @@ -0,0 +1,134 @@
    +/*
    + *  Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    + *
    + *  Modifications copyright (C) 2017 Uber Technologies, Inc.
    + *
    + *  Licensed under the Apache License, Version 2.0 (the "License"). You may not
    + *  use this file except in compliance with the License. A copy of the License is
    + *  located at
    + *
    + *  http://aws.amazon.com/apache2.0
    + *
    + *  or in the "license" file accompanying this file. This file is distributed on
    + *  an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
    + *  express or implied. See the License for the specific language governing
    + *  permissions and limitations under the License.
    + */
    +
    +package com.uber.cadence.testing;
    +
    +import static org.junit.Assert.*;
    +
    +import com.uber.cadence.client.WorkflowClient;
    +import com.uber.cadence.client.WorkflowOptions;
    +import com.uber.cadence.worker.Worker;
    +import com.uber.cadence.workflow.GetVersionOptions;
    +import com.uber.cadence.workflow.Workflow;
    +import com.uber.cadence.workflow.WorkflowMethod;
    +import java.time.Duration;
    +import java.util.concurrent.ExecutionException;
    +import org.junit.Test;
    +
    +/**
    + * Test to verify that TestWorkflowEnvironment supports the new GetVersionOptions functionality.
    + * This test ensures that workflows using enhanced version control features can be tested properly.
    + */
    +public class TestWorkflowEnvironmentGetVersionTest {
    +
    +  private static final String TASK_LIST = "TestWorkflowEnvironmentGetVersionTest";
    +
    +  public interface TestWorkflowWithVersionControl {
    +    @WorkflowMethod
    +    String workflowWithVersionControl(String input);
    +  }
    +
    +  public static class TestWorkflowWithVersionControlImpl implements TestWorkflowWithVersionControl {
    +
    +    @Override
    +    public String workflowWithVersionControl(String input) {
    +      // Test the new getVersion method with options
    +      GetVersionOptions options = GetVersionOptions.newBuilder().executeWithVersion(2).build();
    +
    +      int version = Workflow.getVersion("test-change", 1, 3, options);
    +
    +      // Test the convenience methods
    +      int versionWithCustom = Workflow.getVersionWithCustomVersion("test-change", 1, 3, 2);
    +      int versionWithMin = Workflow.getVersionWithMinVersion("test-change", 1, 3);
    +
    +      return String.format(
    +          "input=%s, version=%d, custom=%d, min=%d",
    +          input, version, versionWithCustom, versionWithMin);
    +    }
    +  }
    +
    +  @Test
    +  public void testWorkflowWithVersionControl() throws ExecutionException, InterruptedException {
    +    TestWorkflowEnvironment testEnvironment = TestWorkflowEnvironment.newInstance();
    +
    +    // Create a worker that polls tasks from the service owned by the testEnvironment
    +    Worker worker = testEnvironment.newWorker(TASK_LIST);
    +    worker.registerWorkflowImplementationTypes(TestWorkflowWithVersionControlImpl.class);
    +
    +    // Create a WorkflowClient that interacts with the server owned by the testEnvironment
    +    WorkflowClient client = testEnvironment.newWorkflowClient();
    +
    +    // Create workflow options with required timeout
    +    WorkflowOptions options =
    +        new WorkflowOptions.Builder()
    +            .setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
    +            .setTaskList(TASK_LIST)
    +            .build();
    +
    +    TestWorkflowWithVersionControl workflow =
    +        client.newWorkflowStub(TestWorkflowWithVersionControl.class, options);
    +
    +    // Start the test environment (this starts the worker)
    +    testEnvironment.start();
    +
    +    // Start a workflow execution
    +    String result = workflow.workflowWithVersionControl("test-input");
    +
    +    // Verify the result contains the expected version information
    +    assertNotNull("Result should not be null", result);
    +    assertTrue("Result should contain version information", result.contains("version="));
    +    assertTrue("Result should contain custom version information", result.contains("custom="));
    +    assertTrue("Result should contain min version information", result.contains("min="));
    +
    +    // Close workers and release in-memory service
    +    testEnvironment.close();
    +  }
    +
    +  @Test
    +  public void testGetVersionOptionsBuilder() {
    +    // Test the builder pattern for GetVersionOptions
    +    GetVersionOptions options1 = GetVersionOptions.newBuilder().executeWithVersion(5).build();
    +
    +    assertTrue("Should have custom version", options1.getCustomVersion().isPresent());
    +    assertEquals(
    +        "Custom version should be 5", Integer.valueOf(5), options1.getCustomVersion().get());
    +    assertFalse("Should not use min version", options1.isUseMinVersion());
    +
    +    GetVersionOptions options2 = GetVersionOptions.newBuilder().executeWithMinVersion().build();
    +
    +    assertFalse("Should not have custom version", options2.getCustomVersion().isPresent());
    +    assertTrue("Should use min version", options2.isUseMinVersion());
    +
    +    GetVersionOptions options3 =
    +        GetVersionOptions.newBuilder().executeWithVersion(10).executeWithMinVersion().build();
    +
    +    assertTrue("Should have custom version", options3.getCustomVersion().isPresent());
    +    assertEquals(
    +        "Custom version should be 10", Integer.valueOf(10), options3.getCustomVersion().get());
    +    assertTrue("Should use min version", options3.isUseMinVersion());
    +  }
    +
    +  @Test
    +  public void testDefaultGetVersionOptions() {
    +    // Test default options
    +    GetVersionOptions defaultOptions = GetVersionOptions.newBuilder().build();
    +
    +    assertFalse(
    +        "Default should not have custom version", defaultOptions.getCustomVersion().isPresent());
    +    assertFalse("Default should not use min version", defaultOptions.isUseMinVersion());
    +  }
    +}
    diff --git a/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java b/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java
    index 5de40d743..da122493d 100644
    --- a/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java
    +++ b/src/test/java/com/uber/cadence/workflow/EnhancedGetVersionTest.java
    @@ -19,8 +19,12 @@
     
     import static org.junit.Assert.*;
     
    +import com.uber.cadence.client.WorkflowClient;
    +import com.uber.cadence.client.WorkflowOptions;
     import com.uber.cadence.testing.TestWorkflowEnvironment;
     import com.uber.cadence.worker.Worker;
    +import java.time.Duration;
    +import java.util.concurrent.ExecutionException;
     import org.junit.After;
     import org.junit.Before;
     import org.junit.Test;
    @@ -42,46 +46,82 @@ public void tearDown() {
       }
     
       @Test
    -  public void testGetVersionWithCustomVersion() {
    -    worker.registerWorkflowImplementationTypes(TestWorkflowWithCustomVersion.class);
    +  public void testGetVersionWithCustomVersion() throws ExecutionException, InterruptedException {
    +    worker.registerWorkflowImplementationTypes(TestWorkflowWithCustomVersionImpl.class);
         testEnvironment.start();
     
    -    TestWorkflowWithCustomVersion workflow = testEnvironment.newWorkflowStub(TestWorkflowWithCustomVersion.class);
    +    WorkflowClient client = testEnvironment.newWorkflowClient();
    +
    +    WorkflowOptions options =
    +        new WorkflowOptions.Builder()
    +            .setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
    +            .setTaskList("test-task-list")
    +            .build();
    +
    +    TestWorkflowWithCustomVersion workflow =
    +        client.newWorkflowStub(TestWorkflowWithCustomVersion.class, options);
         String result = workflow.execute("test-input");
    -    
    +
         assertEquals("custom-version-result", result);
       }
     
       @Test
    -  public void testGetVersionWithMinVersion() {
    -    worker.registerWorkflowImplementationTypes(TestWorkflowWithMinVersion.class);
    +  public void testGetVersionWithMinVersion() throws ExecutionException, InterruptedException {
    +    worker.registerWorkflowImplementationTypes(TestWorkflowWithMinVersionImpl.class);
         testEnvironment.start();
     
    -    TestWorkflowWithMinVersion workflow = testEnvironment.newWorkflowStub(TestWorkflowWithMinVersion.class);
    +    WorkflowClient client = testEnvironment.newWorkflowClient();
    +
    +    WorkflowOptions options =
    +        new WorkflowOptions.Builder()
    +            .setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
    +            .setTaskList("test-task-list")
    +            .build();
    +
    +    TestWorkflowWithMinVersion workflow =
    +        client.newWorkflowStub(TestWorkflowWithMinVersion.class, options);
         String result = workflow.execute("test-input");
    -    
    +
         assertEquals("min-version-result", result);
       }
     
       @Test
    -  public void testGetVersionWithOptions() {
    -    worker.registerWorkflowImplementationTypes(TestWorkflowWithOptions.class);
    +  public void testGetVersionWithOptions() throws ExecutionException, InterruptedException {
    +    worker.registerWorkflowImplementationTypes(TestWorkflowWithOptionsImpl.class);
         testEnvironment.start();
     
    -    TestWorkflowWithOptions workflow = testEnvironment.newWorkflowStub(TestWorkflowWithOptions.class);
    +    WorkflowClient client = testEnvironment.newWorkflowClient();
    +
    +    WorkflowOptions options =
    +        new WorkflowOptions.Builder()
    +            .setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
    +            .setTaskList("test-task-list")
    +            .build();
    +
    +    TestWorkflowWithOptions workflow =
    +        client.newWorkflowStub(TestWorkflowWithOptions.class, options);
         String result = workflow.execute("test-input");
    -    
    +
         assertEquals("options-result", result);
       }
     
       @Test
    -  public void testConvenienceMethods() {
    -    worker.registerWorkflowImplementationTypes(TestWorkflowWithConvenienceMethods.class);
    +  public void testConvenienceMethods() throws ExecutionException, InterruptedException {
    +    worker.registerWorkflowImplementationTypes(TestWorkflowWithConvenienceMethodsImpl.class);
         testEnvironment.start();
     
    -    TestWorkflowWithConvenienceMethods workflow = testEnvironment.newWorkflowStub(TestWorkflowWithConvenienceMethods.class);
    +    WorkflowClient client = testEnvironment.newWorkflowClient();
    +
    +    WorkflowOptions options =
    +        new WorkflowOptions.Builder()
    +            .setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
    +            .setTaskList("test-task-list")
    +            .build();
    +
    +    TestWorkflowWithConvenienceMethods workflow =
    +        client.newWorkflowStub(TestWorkflowWithConvenienceMethods.class, options);
         String result = workflow.execute("test-input");
    -    
    +
         assertEquals("convenience-result", result);
       }
     
    @@ -93,9 +133,10 @@ public interface TestWorkflowWithCustomVersion {
       public static class TestWorkflowWithCustomVersionImpl implements TestWorkflowWithCustomVersion {
         @Override
         public String execute(String input) {
    -      int version = Workflow.getVersion("test-change", 1, 3, 
    -          GetVersionOptions.newBuilder().executeWithVersion(2).build());
    -      
    +      int version =
    +          Workflow.getVersion(
    +              "test-change", 1, 3, GetVersionOptions.newBuilder().executeWithVersion(2).build());
    +
           if (version == 2) {
             return "custom-version-result";
           } else {
    @@ -112,9 +153,10 @@ public interface TestWorkflowWithMinVersion {
       public static class TestWorkflowWithMinVersionImpl implements TestWorkflowWithMinVersion {
         @Override
         public String execute(String input) {
    -      int version = Workflow.getVersion("test-change", 1, 3, 
    -          GetVersionOptions.newBuilder().executeWithMinVersion().build());
    -      
    +      int version =
    +          Workflow.getVersion(
    +              "test-change", 1, 3, GetVersionOptions.newBuilder().executeWithMinVersion().build());
    +
           if (version == 1) {
             return "min-version-result";
           } else {
    @@ -131,12 +173,10 @@ public interface TestWorkflowWithOptions {
       public static class TestWorkflowWithOptionsImpl implements TestWorkflowWithOptions {
         @Override
         public String execute(String input) {
    -      GetVersionOptions options = GetVersionOptions.newBuilder()
    -          .executeWithVersion(2)
    -          .build();
    -      
    +      GetVersionOptions options = GetVersionOptions.newBuilder().executeWithVersion(2).build();
    +
           int version = Workflow.getVersion("test-change", 1, 3, options);
    -      
    +
           if (version == 2) {
             return "options-result";
           } else {
    @@ -150,12 +190,13 @@ public interface TestWorkflowWithConvenienceMethods {
         String execute(String input);
       }
     
    -  public static class TestWorkflowWithConvenienceMethodsImpl implements TestWorkflowWithConvenienceMethods {
    +  public static class TestWorkflowWithConvenienceMethodsImpl
    +      implements TestWorkflowWithConvenienceMethods {
         @Override
         public String execute(String input) {
           int version1 = Workflow.getVersionWithCustomVersion("test-change-1", 1, 3, 2);
           int version2 = Workflow.getVersionWithMinVersion("test-change-2", 1, 3);
    -      
    +
           if (version1 == 2 && version2 == 1) {
             return "convenience-result";
           } else {
    @@ -163,4 +204,4 @@ public String execute(String input) {
           }
         }
       }
    -} 
    \ No newline at end of file
    +}
    diff --git a/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java b/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java
    index c7277c107..6447e2af6 100644
    --- a/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java
    +++ b/src/test/java/com/uber/cadence/workflow/GetVersionOptionsTest.java
    @@ -26,73 +26,61 @@ public class GetVersionOptionsTest {
     
       @Test
       public void testExecuteWithVersion() {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithVersion(5)
    -        .build();
    -    
    +    GetVersionOptions options = GetVersionOptions.newBuilder().executeWithVersion(5).build();
    +
         assertEquals(Optional.of(5), options.getCustomVersion());
         assertFalse(options.isUseMinVersion());
       }
    -  
    +
       @Test
       public void testExecuteWithMinVersion() {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithMinVersion()
    -        .build();
    -    
    +    GetVersionOptions options = GetVersionOptions.newBuilder().executeWithMinVersion().build();
    +
         assertEquals(Optional.empty(), options.getCustomVersion());
         assertTrue(options.isUseMinVersion());
       }
    -  
    +
       @Test
       public void testDefaultOptions() {
         GetVersionOptions options = GetVersionOptions.newBuilder().build();
    -    
    +
         assertEquals(Optional.empty(), options.getCustomVersion());
         assertFalse(options.isUseMinVersion());
       }
    -  
    +
       @Test
       public void testBuilderChaining() {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithVersion(3)
    -        .executeWithMinVersion()
    -        .build();
    -    
    +    GetVersionOptions options =
    +        GetVersionOptions.newBuilder().executeWithVersion(3).executeWithMinVersion().build();
    +
         // When both are set, custom version takes precedence
         assertEquals(Optional.of(3), options.getCustomVersion());
         assertTrue(options.isUseMinVersion());
       }
    -  
    +
       @Test
       public void testMultipleExecuteWithVersionCalls() {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithVersion(1)
    -        .executeWithVersion(2)
    -        .build();
    -    
    +    GetVersionOptions options =
    +        GetVersionOptions.newBuilder().executeWithVersion(1).executeWithVersion(2).build();
    +
         // Last call should take precedence
         assertEquals(Optional.of(2), options.getCustomVersion());
         assertFalse(options.isUseMinVersion());
       }
    -  
    +
       @Test
       public void testZeroVersion() {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithVersion(0)
    -        .build();
    -    
    +    GetVersionOptions options = GetVersionOptions.newBuilder().executeWithVersion(0).build();
    +
         assertEquals(Optional.of(0), options.getCustomVersion());
         assertFalse(options.isUseMinVersion());
       }
    -  
    +
       @Test
       public void testNegativeVersion() {
    -    GetVersionOptions options = GetVersionOptions.newBuilder()
    -        .executeWithVersion(-1)
    -        .build();
    -    
    +    GetVersionOptions options = GetVersionOptions.newBuilder().executeWithVersion(-1).build();
    +
         assertEquals(Optional.of(-1), options.getCustomVersion());
         assertFalse(options.isUseMinVersion());
       }
    -} 
    \ No newline at end of file
    +}
    diff --git a/src/test/java/com/uber/cadence/workflow/interceptors/SignalWorkflowInterceptor.java b/src/test/java/com/uber/cadence/workflow/interceptors/SignalWorkflowInterceptor.java
    index f4c113e5c..07fb02b94 100644
    --- a/src/test/java/com/uber/cadence/workflow/interceptors/SignalWorkflowInterceptor.java
    +++ b/src/test/java/com/uber/cadence/workflow/interceptors/SignalWorkflowInterceptor.java
    @@ -157,6 +157,12 @@ public int getVersion(String changeID, int minSupported, int maxSupported) {
         return next.getVersion(changeID, minSupported, maxSupported);
       }
     
    +  @Override
    +  public int getVersion(
    +      String changeID, int minSupported, int maxSupported, GetVersionOptions options) {
    +    return next.getVersion(changeID, minSupported, maxSupported, options);
    +  }
    +
       @Override
       public void continueAsNew(
           Optional workflowType, Optional options, Object[] args) {
    diff --git a/src/test/java/com/uber/cadence/workflow/interceptors/TracingWorkflowInterceptorFactory.java b/src/test/java/com/uber/cadence/workflow/interceptors/TracingWorkflowInterceptorFactory.java
    index 80cf10790..63e1712ab 100644
    --- a/src/test/java/com/uber/cadence/workflow/interceptors/TracingWorkflowInterceptorFactory.java
    +++ b/src/test/java/com/uber/cadence/workflow/interceptors/TracingWorkflowInterceptorFactory.java
    @@ -207,6 +207,13 @@ public int getVersion(String changeID, int minSupported, int maxSupported) {
           return next.getVersion(changeID, minSupported, maxSupported);
         }
     
    +    @Override
    +    public int getVersion(
    +        String changeID, int minSupported, int maxSupported, GetVersionOptions options) {
    +      trace.add("getVersion with options");
    +      return next.getVersion(changeID, minSupported, maxSupported, options);
    +    }
    +
         @Override
         public void continueAsNew(
             Optional workflowType, Optional options, Object[] args) {