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 extends WorkflowState> 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