Solutions - AWS Flow Framework for Java

Solutions

You can use the following solutions to avoid backwards-incompatible changes. For more information, see Making Changes to Decider Code and Example Scenario.

Using Versioning

In this solution, you copy the decider to a new class, modify the decider, and then register the decider under a new workflow version.

VersionedDecider.java

package sample.v2; import com.amazonaws.services.simpleworkflow.flow.DecisionContext; import com.amazonaws.services.simpleworkflow.flow.DecisionContextProviderImpl; import com.amazonaws.services.simpleworkflow.flow.WorkflowClock; import com.amazonaws.services.simpleworkflow.flow.annotations.Execute; import com.amazonaws.services.simpleworkflow.flow.annotations.Workflow; import com.amazonaws.services.simpleworkflow.flow.annotations.WorkflowRegistrationOptions; import sample.Input; @Workflow @WorkflowRegistrationOptions(defaultExecutionStartToCloseTimeoutSeconds = 60, defaultTaskStartToCloseTimeoutSeconds = 5) public interface Foo { @Execute(version = "2") public void sample(Input input); public static class Impl implements Foo { private DecisionContext decisionContext = new DecisionContextProviderImpl().getDecisionContext(); private WorkflowClock clock = decisionContext.getWorkflowClock(); @Override public void sample(Input input) { System.out.println("Decision (V2) WorkflowId: " + decisionContext.getWorkflowContext().getWorkflowExecution().getWorkflowId()); clock.createTimer(5); } } }

In the updated Java code, the second decider worker runs both versions of the workflow, allowing in-flight executions to continue to execute independently of the changes in version 2.

RunVersionedDecider.java

package sample; import com.amazonaws.services.simpleworkflow.flow.WorkflowWorker; public class VersionedChange extends SampleBase { public static void main(String[] args) throws Exception { new VersionedChange().run(); } public void run() throws Exception { // Start the first version of the decider, with workflow version 1 WorkflowWorker before = new WorkflowWorker(service, domain, taskList); before.addWorkflowImplementationType(sample.v1.Foo.Impl.class); before.start(); // Start a few executions with version 1 startFiveExecutions("Foo.sample", "1", new Input()); // Stop the first decider worker and wait a few seconds // for its pending pollers to match and return before.suspendPolling(); sleep(2000); // At this point, three executions are still open, with more decisions to make // Start a worker with both the previous version of the decider (workflow version 1) // and the modified code (workflow version 2) WorkflowWorker after = new WorkflowWorker(service, domain, taskList); after.addWorkflowImplementationType(sample.v1.Foo.Impl.class); after.addWorkflowImplementationType(sample.v2.Foo.Impl.class); after.start(); // Start a few more executions with version 2 startFiveExecutions("Foo.sample", "2", new Input()); printExecutionResults(); } }

When you run the program, all executions complete successfully.

Using Feature Flags

Another solution to backwards-compatibility issues is to branch code to support two implementations in the same class is to branch based on input data instead of workflow versions.

When you take this approach, you add fields to (or modify existing fields of) your input objects every time you introduce sensitive changes. For executions that start before the migration, the input object won't have the field (or will have a different value). Thus, you don't have to increase the version number.

Note

If you add new fields, ensure that the JSON deserialization process is backwards-compatible. Objects serialized before the introduction of the field should still successfully deserialize after the migration. Because JSON sets a null value whenever a field is missing, always use boxed types (Boolean instead of boolean) and handle the cases where the value is null.

FeatureFlagDecider.java

package sample.v1.featureflag; import com.amazonaws.services.simpleworkflow.flow.DecisionContext; import com.amazonaws.services.simpleworkflow.flow.DecisionContextProviderImpl; import com.amazonaws.services.simpleworkflow.flow.WorkflowClock; import com.amazonaws.services.simpleworkflow.flow.annotations.Execute; import com.amazonaws.services.simpleworkflow.flow.annotations.Workflow; import com.amazonaws.services.simpleworkflow.flow.annotations.WorkflowRegistrationOptions; import sample.Input; @Workflow @WorkflowRegistrationOptions(defaultExecutionStartToCloseTimeoutSeconds = 60, defaultTaskStartToCloseTimeoutSeconds = 5) public interface Foo { @Execute(version = "1") public void sample(Input input); public static class Impl implements Foo { private DecisionContext decisionContext = new DecisionContextProviderImpl().getDecisionContext(); private WorkflowClock clock = decisionContext.getWorkflowClock(); @Override public void sample(Input input) { System.out.println("Decision (V1 feature flag) WorkflowId: " + decisionContext.getWorkflowContext().getWorkflowExecution().getWorkflowId()); clock.createTimer(5); if (!input.getSkipSecondTimer()) { clock.createTimer(5); } } } }

In the updated Java code, the code for both versions of the workflow is still registered for version 1. However, after the migration, new executions start with the skipSecondTimer field of the input data set to true.

RunFeatureFlagDecider.java

package sample; import com.amazonaws.services.simpleworkflow.flow.WorkflowWorker; public class FeatureFlagChange extends SampleBase { public static void main(String[] args) throws Exception { new FeatureFlagChange().run(); } public void run() throws Exception { // Start the first version of the decider WorkflowWorker before = new WorkflowWorker(service, domain, taskList); before.addWorkflowImplementationType(sample.v1.Foo.Impl.class); before.start(); // Start a few executions startFiveExecutions("Foo.sample", "1", new Input()); // Stop the first decider worker and wait a few seconds // for its pending pollers to match and return before.suspendPolling(); sleep(2000); // At this point, three executions are still open, with more decisions to make // Start a new version of the decider that introduces a change // while preserving backwards compatibility based on input fields WorkflowWorker after = new WorkflowWorker(service, domain, taskList); after.addWorkflowImplementationType(sample.v1.featureflag.Foo.Impl.class); after.start(); // Start a few more executions and enable the new feature through the input data startFiveExecutions("Foo.sample", "1", new Input().setSkipSecondTimer(true)); printExecutionResults(); } }

When you run the program, all executions complete successfully.