diff --git a/src/main/java/io/iworkflow/core/Client.java b/src/main/java/io/iworkflow/core/Client.java index 1bda5ec9..847d28e9 100644 --- a/src/main/java/io/iworkflow/core/Client.java +++ b/src/main/java/io/iworkflow/core/Client.java @@ -9,6 +9,7 @@ import io.iworkflow.gen.models.WorkflowGetSearchAttributesResponse; import io.iworkflow.gen.models.WorkflowSearchRequest; import io.iworkflow.gen.models.WorkflowSearchResponse; +import io.iworkflow.gen.models.WorkflowStateOptions; import java.util.ArrayList; import java.util.HashMap; @@ -16,6 +17,8 @@ import java.util.Map; import java.util.stream.Collectors; +import static io.iworkflow.core.WorkflowState.shouldSkipWaitUntil; + public class Client { private final Registry registry; @@ -159,8 +162,16 @@ private String doStartWorkflow( throw new WorkflowDefinitionException(String.format("input cannot be assigned to the starting state, input type: %s, starting state input type: %s", input.getClass(), registeredInputType)); } - if (stateDef.getWorkflowState().getStateOptions() != null) { - unregisterWorkflowOptions.startStateOptions(stateDef.getWorkflowState().getStateOptions()); + WorkflowStateOptions stateOptions = stateDef.getWorkflowState().getStateOptions(); + if (shouldSkipWaitUntil(stateDef.getWorkflowState())) { + if (stateOptions == null) { + stateOptions = new WorkflowStateOptions().skipWaitUntil(true); + } else { + stateOptions.skipWaitUntil(true); + } + } + if (stateOptions != null) { + unregisterWorkflowOptions.startStateOptions(stateOptions); } if (options != null) { unregisterWorkflowOptions.workflowIdReusePolicy(options.getWorkflowIdReusePolicy()); diff --git a/src/main/java/io/iworkflow/core/ObjectWorkflow.java b/src/main/java/io/iworkflow/core/ObjectWorkflow.java index c2d04960..7ea94246 100644 --- a/src/main/java/io/iworkflow/core/ObjectWorkflow.java +++ b/src/main/java/io/iworkflow/core/ObjectWorkflow.java @@ -8,10 +8,7 @@ /** * This is the interface to define an object workflow definition. - * Most of the time, the implementation only needs to return static value for each method. - *

- * For a dynamic workflow definition, the implementation can return different values based on different constructor inputs. - * To invokes/interact with a dynamic workflows, applications may need to use {@link UnregisteredClient} instead of {@link Client} + * ObjectWorkflow is a top level concept in iWF. Any object that is long-lasting(at least a few seconds) can be modeled as an "ObjectWorkflow". */ public interface ObjectWorkflow { /** diff --git a/src/main/java/io/iworkflow/core/WorkflowState.java b/src/main/java/io/iworkflow/core/WorkflowState.java index 6a2396fa..3576f497 100644 --- a/src/main/java/io/iworkflow/core/WorkflowState.java +++ b/src/main/java/io/iworkflow/core/WorkflowState.java @@ -6,6 +6,8 @@ import io.iworkflow.core.persistence.Persistence; import io.iworkflow.gen.models.WorkflowStateOptions; +import java.lang.reflect.Method; + public interface WorkflowState { /** @@ -15,7 +17,13 @@ public interface WorkflowState { Class getInputType(); /** - * Implement this method to execute the commands set up condition for the {@link #execute} API + * Optionally implement this method to set up condition for the state. + * If implemented, this will be the first API invoked when state started. + * Then the state will be waiting until the requested commands are completed. + * If not implemented, the state will invoke the {@link #execute} directly + *

+ * The condition is setup using commands. There are three types commands in a {@link CommandRequest}: signal, timer and interStateChannel; + * Also with three types of {@link io.iworkflow.gen.models.CommandWaitingType} * * @param context the context info of this API invocation, like workflow start time, workflowId, etc * @param input the state input which is deserialized by {@link ObjectEncoder} with {@link #getInputType} @@ -28,13 +36,20 @@ public interface WorkflowState { * Note that any write API will be recorded to server after the whole start API response is accepted. * @return the requested commands for this step */ - CommandRequest waitUntil( + default CommandRequest waitUntil( final Context context, I input, final Persistence persistence, - final Communication communication); + final Communication communication) { + /* + * leaving this method with default implementation means the state doesn't have any condition for setup. + * iWF will omit the waitUntil step and invoke the {@link #execute} API directly + */ + throw new IllegalStateException("this exception will never be thrown."); + } /** - * Implement this method to execute the state business, when requested commands are ready + * Implement this method to execute the state business, when requested commands are ready if {@link #waitUntil} is implemented + * If {@link #waitUntil} is not implemented, the state will invoke this API directly * * @param context the context info of this API invocation, like workflow start time, workflowId, etc * @param input the state input which is deserialized by {@link ObjectEncoder} with {@link #getInputType} @@ -82,6 +97,20 @@ default String getStateId() { default WorkflowStateOptions getStateOptions() { return null; } + + static boolean shouldSkipWaitUntil(final WorkflowState state) { + final Class stateClass = state.getClass(); + final Method waitUntilMethod; + try { + waitUntilMethod = stateClass.getMethod("waitUntil", Context.class, Object.class, Persistence.class, Communication.class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + if (waitUntilMethod.getDeclaringClass().equals(WorkflowState.class)) { + return true; + } + return false; + } } diff --git a/src/main/java/io/iworkflow/core/mapper/StateMovementMapper.java b/src/main/java/io/iworkflow/core/mapper/StateMovementMapper.java index 723f0046..9c66f63e 100644 --- a/src/main/java/io/iworkflow/core/mapper/StateMovementMapper.java +++ b/src/main/java/io/iworkflow/core/mapper/StateMovementMapper.java @@ -7,6 +7,7 @@ import io.iworkflow.gen.models.WorkflowStateOptions; import static io.iworkflow.core.StateMovement.RESERVED_STATE_ID_PREFIX; +import static io.iworkflow.core.WorkflowState.shouldSkipWaitUntil; public class StateMovementMapper { @@ -17,9 +18,16 @@ public static StateMovement toGenerated(io.iworkflow.core.StateMovement stateMov .stateInput(objectEncoder.encode(input)); if (!stateMovement.getStateId().startsWith(RESERVED_STATE_ID_PREFIX)) { final StateDef stateDef = registry.getWorkflowState(workflowType, stateMovement.getStateId()); - final WorkflowStateOptions options = stateDef.getWorkflowState().getStateOptions(); - if (options != null) { - movement.stateOptions(options); + WorkflowStateOptions stateOptions = stateDef.getWorkflowState().getStateOptions(); + if (shouldSkipWaitUntil(stateDef.getWorkflowState())) { + if (stateOptions == null) { + stateOptions = new WorkflowStateOptions().skipWaitUntil(true); + } else { + stateOptions.skipWaitUntil(true); + } + } + if (stateOptions != null) { + movement.stateOptions(stateOptions); } } return movement; diff --git a/src/test/java/io/iworkflow/integ/SkipWaitUntilTest.java b/src/test/java/io/iworkflow/integ/SkipWaitUntilTest.java new file mode 100644 index 00000000..e44700ab --- /dev/null +++ b/src/test/java/io/iworkflow/integ/SkipWaitUntilTest.java @@ -0,0 +1,39 @@ +package io.iworkflow.integ; + +import io.iworkflow.core.Client; +import io.iworkflow.core.ClientOptions; +import io.iworkflow.core.ImmutableWorkflowOptions; +import io.iworkflow.core.WorkflowOptions; +import io.iworkflow.gen.models.WorkflowConfig; +import io.iworkflow.gen.models.WorkflowIDReusePolicy; +import io.iworkflow.integ.basic.SkipWaitUntilWorkflow; +import io.iworkflow.spring.TestSingletonWorkerService; +import io.iworkflow.spring.controller.WorkflowRegistry; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutionException; + +public class SkipWaitUntilTest { + + @BeforeEach + public void setup() throws ExecutionException, InterruptedException { + TestSingletonWorkerService.startWorkerIfNotUp(); + } + + @Test + public void testSkipWaitUntil() throws InterruptedException { + final Client client = new Client(WorkflowRegistry.registry, ClientOptions.localDefault); + final String wfId = "testSkipWaitUntil-" + System.currentTimeMillis() / 1000; + final WorkflowOptions startOptions = ImmutableWorkflowOptions.builder() + .workflowIdReusePolicy(WorkflowIDReusePolicy.REJECT_DUPLICATE) + .workflowConfigOverride(new WorkflowConfig().continueAsNewThreshold(1)) + .build(); + final int input = 0; + client.startWorkflow(SkipWaitUntilWorkflow.class, wfId, 10, input, startOptions); + // wait for workflow to finish + final Integer output = client.getSimpleWorkflowResultWithWait(Integer.class, wfId); + Assertions.assertEquals(input + 2, output); + } +} diff --git a/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilState1.java b/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilState1.java new file mode 100644 index 00000000..d6655a91 --- /dev/null +++ b/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilState1.java @@ -0,0 +1,22 @@ +package io.iworkflow.integ.basic; + +import io.iworkflow.core.Context; +import io.iworkflow.core.StateDecision; +import io.iworkflow.core.WorkflowState; +import io.iworkflow.core.command.CommandResults; +import io.iworkflow.core.communication.Communication; +import io.iworkflow.core.persistence.Persistence; + +public class SkipWaitUntilState1 implements WorkflowState { + + @Override + public Class getInputType() { + return Integer.class; + } + + @Override + public StateDecision execute(final Context context, final Integer input, final CommandResults commandResults, Persistence persistence, final Communication communication) { + final int output = input + 1; + return StateDecision.singleNextState(SkipWaitUntilState2.class, output); + } +} \ No newline at end of file diff --git a/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilState2.java b/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilState2.java new file mode 100644 index 00000000..eb7d7aa1 --- /dev/null +++ b/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilState2.java @@ -0,0 +1,22 @@ +package io.iworkflow.integ.basic; + +import io.iworkflow.core.Context; +import io.iworkflow.core.StateDecision; +import io.iworkflow.core.WorkflowState; +import io.iworkflow.core.command.CommandResults; +import io.iworkflow.core.communication.Communication; +import io.iworkflow.core.persistence.Persistence; + +public class SkipWaitUntilState2 implements WorkflowState { + + @Override + public Class getInputType() { + return Integer.class; + } + + @Override + public StateDecision execute(final Context context, final Integer input, final CommandResults commandResults, Persistence persistence, final Communication communication) { + final int output = input + 1; + return StateDecision.gracefulCompleteWorkflow(output); + } +} \ No newline at end of file diff --git a/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilWorkflow.java b/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilWorkflow.java new file mode 100644 index 00000000..51245f16 --- /dev/null +++ b/src/test/java/io/iworkflow/integ/basic/SkipWaitUntilWorkflow.java @@ -0,0 +1,20 @@ +package io.iworkflow.integ.basic; + +import io.iworkflow.core.ObjectWorkflow; +import io.iworkflow.core.StateDef; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +@Component +public class SkipWaitUntilWorkflow implements ObjectWorkflow { + + @Override + public List getWorkflowStates() { + return Arrays.asList( + StateDef.startingState(new SkipWaitUntilState1()), + StateDef.nonStartingState(new SkipWaitUntilState2()) + ); + } +}