Skip to content

Commit 757eed4

Browse files
authored
Support overriding state options (#193)
Co-authored-by: Kaili Zhu <kzhu@indeed.com>
1 parent 6f1ffd6 commit 757eed4

File tree

5 files changed

+239
-46
lines changed

5 files changed

+239
-46
lines changed

src/main/java/io/iworkflow/core/StateDecision.java

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.iworkflow.core;
22

33
import io.iworkflow.gen.models.WorkflowConditionalCloseType;
4+
import io.iworkflow.gen.models.WorkflowStateOptions;
45
import org.immutables.value.Value;
56

67
import java.util.ArrayList;
@@ -145,54 +146,79 @@ public static StateDecision forceCompleteIfSignalChannelEmptyOrElse(final Object
145146
.build();
146147
}
147148

148-
public static StateDecision singleNextState(final Class<? extends WorkflowState> stateClass) {
149-
return singleNextState(stateClass.getSimpleName());
149+
/**
150+
*
151+
* @param stateClass required
152+
* @param stateInput optional, can be null
153+
* @param stateOptionsOverride optional, can be null. It is used to override the defined one in the State class
154+
* @return state decision
155+
*/
156+
public static StateDecision singleNextState(final Class<? extends WorkflowState> stateClass, final Object stateInput, final WorkflowStateOptions stateOptionsOverride) {
157+
return singleNextState(stateClass.getSimpleName(), stateInput, stateOptionsOverride);
150158
}
151159

152160
/**
153-
* use the other one with WorkflowState class param if the StateId is provided by default, to make your code cleaner
154161
*
155-
* @param stateId stateId
162+
* @param stateClass required
163+
* @param stateInput optional, can be null
156164
* @return state decision
157165
*/
158-
public static StateDecision singleNextState(final String stateId) {
159-
return ImmutableStateDecision.builder().nextStates(Arrays.asList(
160-
StateMovement.create(stateId)
161-
)).build();
166+
public static StateDecision singleNextState(final Class<? extends WorkflowState> stateClass, final Object stateInput) {
167+
return singleNextState(stateClass, stateInput, null);
162168
}
163169

164-
public static StateDecision singleNextState(final Class<? extends WorkflowState> stateClass, final Object stateInput) {
165-
return singleNextState(stateClass.getSimpleName(), stateInput);
170+
/**
171+
*
172+
* @param stateClass required
173+
* @return state decision
174+
*/
175+
public static StateDecision singleNextState(final Class<? extends WorkflowState> stateClass) {
176+
return singleNextState(stateClass, null, null);
166177
}
167178

168179
/**
169-
* use the other one with WorkflowState class param if the StateId is provided by default, to make your code cleaner
170-
* @param stateId stateId of next state
171-
* @param stateInput input for next state
180+
* use the other one with WorkflowState class param if the stateId is provided by default, to make your code cleaner
181+
* @param stateId required. StateId of next state
182+
* @param stateInput optional, can be null. Input for next state
183+
* @param stateOptionsOverride optional, can be null. It is used to override the defined one in the State class
172184
* @return state decision
173185
*/
174-
public static StateDecision singleNextState(final String stateId, final Object stateInput) {
186+
public static StateDecision singleNextState(final String stateId, final Object stateInput, final WorkflowStateOptions stateOptionsOverride) {
175187
return ImmutableStateDecision.builder().nextStates(Arrays.asList(
176-
StateMovement.create(stateId, stateInput)
188+
StateMovement.create(stateId, stateInput, stateOptionsOverride)
177189
)).build();
178190
}
179191

180-
public static StateDecision multiNextStates(final StateMovement... stateMovements) {
181-
return ImmutableStateDecision.builder().nextStates(Arrays.asList(stateMovements)).build();
192+
/**
193+
* use the other one with WorkflowState class param if the stateId is provided by default, to make your code cleaner
194+
* @param stateId stateId of next state
195+
* @return state decision
196+
*/
197+
public static StateDecision singleNextState(final String stateId) {
198+
return singleNextState(stateId, null, null);
182199
}
183200

201+
/**
202+
*
203+
* @param stateMovements required
204+
* @return state decision
205+
*/
184206
public static StateDecision multiNextStates(final List<StateMovement> stateMovements) {
185207
return ImmutableStateDecision.builder().nextStates(stateMovements).build();
186208
}
187209

188-
public static StateDecision multiNextStates(final Class<? extends WorkflowState>... states) {
189-
List<String> stateIds = new ArrayList<>();
190-
Arrays.stream(states).forEach(s -> stateIds.add(s.getSimpleName()));
191-
return multiNextStates(stateIds.toArray(new String[0]));
210+
/**
211+
*
212+
* @param stateMovements required
213+
* @return state decision
214+
*/
215+
public static StateDecision multiNextStates(final StateMovement... stateMovements) {
216+
return multiNextStates(Arrays.asList(stateMovements));
192217
}
193218

194219
/**
195-
* use the other one with WorkflowState class param if the StateId is provided by default, to make your code cleaner
220+
* use the other one with WorkflowState class param if the stateId is provided by default, to make your code cleaner
221+
* or use other ones with a list of StateMovement to enable the WorkflowStateOptions overriding
196222
* @param stateIds stateIds of next states
197223
* @return state decision
198224
*/
@@ -201,6 +227,17 @@ public static StateDecision multiNextStates(final String... stateIds) {
201227
Arrays.stream(stateIds).forEach(id -> {
202228
stateMovements.add(StateMovement.create(id));
203229
});
204-
return ImmutableStateDecision.builder().nextStates(stateMovements).build();
230+
return multiNextStates(stateMovements);
231+
}
232+
233+
/**
234+
* use other ones with a list of StateMovement to enable the WorkflowStateOptions overriding
235+
* @param states required
236+
* @return state decision
237+
*/
238+
public static StateDecision multiNextStates(final Class<? extends WorkflowState>... states) {
239+
List<String> stateIds = new ArrayList<>();
240+
Arrays.stream(states).forEach(s -> stateIds.add(s.getSimpleName()));
241+
return multiNextStates(stateIds.toArray(new String[0]));
205242
}
206243
}

src/main/java/io/iworkflow/core/StateMovement.java

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.iworkflow.core;
22

3+
import io.iworkflow.gen.models.WorkflowStateOptions;
34
import org.immutables.value.Value;
45

56
import java.util.Optional;
@@ -10,6 +11,7 @@ public abstract class StateMovement {
1011
public abstract String getStateId();
1112

1213
public abstract Optional<Object> getStateInput();
14+
public abstract Optional<WorkflowStateOptions> getStateOptionsOverride();
1315

1416
public final static String RESERVED_STATE_ID_PREFIX = "_SYS_";
1517
private final static String GRACEFUL_COMPLETING_WORKFLOW_STATE_ID = "_SYS_GRACEFUL_COMPLETING_WORKFLOW";
@@ -50,40 +52,68 @@ public static StateMovement forceFailWorkflow(final Object output) {
5052
.build();
5153
}
5254

53-
public static StateMovement create(final Class<? extends WorkflowState> stateClass, final Object stateInput) {
54-
return create(stateClass.getSimpleName(), stateInput);
55+
/**
56+
*
57+
* @param stateClass required
58+
* @param stateInput optional, can be null
59+
* @param stateOptionsOverride optional, can be null. It is used to override the defined one in the State class
60+
* @return state movement
61+
*/
62+
public static StateMovement create(final Class<? extends WorkflowState> stateClass, final Object stateInput, final WorkflowStateOptions stateOptionsOverride) {
63+
return create(stateClass.getSimpleName(), stateInput, stateOptionsOverride);
5564
}
5665

5766
/**
58-
* use the other one with WorkflowState class param if the StateId is provided by default, to make your code cleaner
5967
*
60-
* @param stateId stateId
61-
* @param stateInput input
68+
* @param stateClass required
69+
* @param stateInput optional, can be null
6270
* @return state movement
6371
*/
64-
public static StateMovement create(final String stateId, final Object stateInput) {
65-
if (stateId.startsWith(RESERVED_STATE_ID_PREFIX)) {
66-
throw new WorkflowDefinitionException("Cannot use reserved stateId prefix for your stateId");
67-
}
68-
return ImmutableStateMovement.builder().stateId(stateId)
69-
.stateInput(stateInput)
70-
.build();
72+
public static StateMovement create(final Class<? extends WorkflowState> stateClass, final Object stateInput) {
73+
return create(stateClass, stateInput, null);
7174
}
7275

76+
/**
77+
*
78+
* @param stateClass required
79+
* @return state movement
80+
*/
7381
public static StateMovement create(final Class<? extends WorkflowState> stateClass) {
74-
return create(stateClass.getSimpleName());
82+
return create(stateClass, null, null);
7583
}
7684

7785
/**
78-
* use the other one with WorkflowState class param if the StateId is provided by default, to make your code cleaner
79-
* @param stateId stateId
86+
* use the other one with WorkflowState class param if the stateId is provided by default, to make your code cleaner
87+
* @param stateId required
88+
* @param stateInput optional, can be null
89+
* @param stateOptionsOverride optional, can be null. It is used to override the defined one in the State class
8090
* @return state movement
8191
*/
82-
public static StateMovement create(final String stateId) {
92+
public static StateMovement create(final String stateId, final Object stateInput, final WorkflowStateOptions stateOptionsOverride) {
8393
if (stateId.startsWith(RESERVED_STATE_ID_PREFIX)) {
8494
throw new WorkflowDefinitionException("Cannot use reserved stateId prefix for your stateId");
8595
}
86-
return ImmutableStateMovement.builder().stateId(stateId)
87-
.build();
96+
97+
final ImmutableStateMovement.Builder builder = ImmutableStateMovement.builder()
98+
.stateId(stateId);
99+
100+
if (stateInput != null) {
101+
builder.stateInput(stateInput);
102+
}
103+
104+
if (stateOptionsOverride != null) {
105+
builder.stateOptionsOverride(stateOptionsOverride);
106+
}
107+
108+
return builder.build();
109+
}
110+
111+
/**
112+
* use the other one with WorkflowState class param if the stateId is provided by default, to make your code cleaner
113+
* @param stateId stateId
114+
* @return state movement
115+
*/
116+
public static StateMovement create(final String stateId) {
117+
return create(stateId, null, null);
88118
}
89119
}

src/main/java/io/iworkflow/core/mapper/StateMovementMapper.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
public class StateMovementMapper {
1313

14-
public static StateMovement toGenerated(io.iworkflow.core.StateMovement stateMovement, final String workflowType, final Registry registry, final ObjectEncoder objectEncoder) {
14+
public static StateMovement toGenerated(final io.iworkflow.core.StateMovement stateMovement, final String workflowType, final Registry registry, final ObjectEncoder objectEncoder) {
1515
final Object input = stateMovement.getStateInput().orElse(null);
1616
final StateMovement movement = new StateMovement()
1717
.stateId(stateMovement.getStateId())
@@ -21,14 +21,21 @@ public static StateMovement toGenerated(io.iworkflow.core.StateMovement stateMov
2121
if(stateDef == null){
2222
throw new IllegalArgumentException("state "+stateMovement.getStateId() +" is not registered in the workflow "+workflowType);
2323
}
24-
WorkflowStateOptions stateOptions = stateDef.getWorkflowState().getStateOptions();
24+
25+
// Try to get the overrode stateOptions, if it's null, get the stateOptions from stateDef
26+
WorkflowStateOptions stateOptions = stateMovement.getStateOptionsOverride().orElse(null);
27+
if (stateOptions == null) {
28+
stateOptions = stateDef.getWorkflowState().getStateOptions();
29+
}
30+
2531
if (shouldSkipWaitUntil(stateDef.getWorkflowState())) {
2632
if (stateOptions == null) {
27-
stateOptions = new WorkflowStateOptions().skipWaitUntil(true);
28-
} else {
29-
stateOptions.skipWaitUntil(true);
33+
stateOptions = new WorkflowStateOptions();
3034
}
35+
36+
stateOptions.skipWaitUntil(true);
3137
}
38+
3239
if (stateOptions != null) {
3340
movement.stateOptions(stateOptions);
3441
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.iworkflow.integ;
2+
3+
import io.iworkflow.core.Client;
4+
import io.iworkflow.core.ClientOptions;
5+
import io.iworkflow.core.ImmutableWorkflowOptions;
6+
import io.iworkflow.core.WorkflowOptions;
7+
import io.iworkflow.gen.models.IDReusePolicy;
8+
import io.iworkflow.integ.stateoptionsoverride.StateOptionsOverrideWorkflow;
9+
import io.iworkflow.spring.TestSingletonWorkerService;
10+
import io.iworkflow.spring.controller.WorkflowRegistry;
11+
import org.junit.jupiter.api.Assertions;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
15+
import java.util.concurrent.ExecutionException;
16+
17+
public class StateOptionsOverrideTest {
18+
@BeforeEach
19+
public void setup() throws ExecutionException, InterruptedException {
20+
TestSingletonWorkerService.startWorkerIfNotUp();
21+
}
22+
@Test
23+
public void testStateOptionsOverrideWorkflow() throws InterruptedException {
24+
final Client client = new Client(WorkflowRegistry.registry, ClientOptions.localDefault);
25+
final String wfId = "state-options-override-test-id" + System.currentTimeMillis() / 1000;
26+
final WorkflowOptions startOptions = ImmutableWorkflowOptions.builder()
27+
.workflowIdReusePolicy(IDReusePolicy.DISALLOW_REUSE)
28+
.build();
29+
final String input = "input";
30+
client.startWorkflow(StateOptionsOverrideWorkflow.class, wfId, 10, input, startOptions);
31+
// wait for workflow to finish
32+
final String output = client.getSimpleWorkflowResultWithWait(String.class, wfId);
33+
Assertions.assertEquals("input_state1_start_state1_decide_state2_start_state2_decide", output);
34+
}
35+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.iworkflow.integ.stateoptionsoverride;
2+
3+
import io.iworkflow.core.Context;
4+
import io.iworkflow.core.ObjectWorkflow;
5+
import io.iworkflow.core.StateDecision;
6+
import io.iworkflow.core.StateDef;
7+
import io.iworkflow.core.WorkflowState;
8+
import io.iworkflow.core.command.CommandRequest;
9+
import io.iworkflow.core.command.CommandResults;
10+
import io.iworkflow.core.communication.Communication;
11+
import io.iworkflow.core.persistence.Persistence;
12+
import io.iworkflow.gen.models.RetryPolicy;
13+
import io.iworkflow.gen.models.WaitUntilApiFailurePolicy;
14+
import io.iworkflow.gen.models.WorkflowStateOptions;
15+
import org.springframework.stereotype.Component;
16+
17+
import java.util.Arrays;
18+
import java.util.List;
19+
20+
@Component
21+
public class StateOptionsOverrideWorkflow implements ObjectWorkflow {
22+
23+
@Override
24+
public List<StateDef> getWorkflowStates() {
25+
return Arrays.asList(
26+
StateDef.startingState(new StateOptionsOverrideWorkflowState1()),
27+
StateDef.nonStartingState(new StateOptionsOverrideWorkflowState2())
28+
);
29+
}
30+
}
31+
32+
class StateOptionsOverrideWorkflowState1 implements WorkflowState<String> {
33+
private String output = "";
34+
35+
@Override
36+
public Class<String> getInputType() {
37+
return String.class;
38+
}
39+
40+
@Override
41+
public CommandRequest waitUntil(Context context, String input, Persistence persistence, Communication communication) {
42+
output = input + "_state1_start";
43+
return CommandRequest.empty;
44+
}
45+
46+
@Override
47+
public StateDecision execute(Context context, String input, CommandResults commandResults, Persistence persistence, Communication communication) {
48+
output = output + "_state1_decide";
49+
return StateDecision.singleNextState(
50+
StateOptionsOverrideWorkflowState2.class, output,
51+
new WorkflowStateOptions()
52+
.waitUntilApiRetryPolicy(new RetryPolicy().maximumAttempts(2))
53+
.waitUntilApiFailurePolicy(WaitUntilApiFailurePolicy.PROCEED_ON_FAILURE)
54+
);
55+
}
56+
}
57+
58+
class StateOptionsOverrideWorkflowState2 implements WorkflowState<String> {
59+
private String output = "";
60+
61+
@Override
62+
public Class<String> getInputType() {
63+
return String.class;
64+
}
65+
66+
@Override
67+
public CommandRequest waitUntil(Context context, String input, Persistence persistence, Communication communication) {
68+
output = input + "_state2_start";
69+
throw new RuntimeException("");
70+
}
71+
72+
@Override
73+
public StateDecision execute(Context context, String input, CommandResults commandResults, Persistence persistence, Communication communication) {
74+
output = output + "_state2_decide";
75+
return StateDecision.gracefulCompleteWorkflow(output);
76+
}
77+
78+
@Override
79+
public WorkflowStateOptions getStateOptions() {
80+
return new WorkflowStateOptions()
81+
.waitUntilApiRetryPolicy(new RetryPolicy().maximumAttempts(1))
82+
.waitUntilApiFailurePolicy(WaitUntilApiFailurePolicy.FAIL_WORKFLOW_ON_FAILURE);
83+
}
84+
}

0 commit comments

Comments
 (0)