Skip to content

Commit a221910

Browse files
authored
Refactored WaitForCondition parameters, WaitForConditionConfig and WaitStrategies (#226)
1 parent 4f7da10 commit a221910

16 files changed

Lines changed: 798 additions & 749 deletions

File tree

examples/src/main/java/software/amazon/lambda/durable/examples/WaitForConditionExample.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,31 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable.examples;
44

5-
import java.time.Duration;
65
import software.amazon.lambda.durable.DurableContext;
76
import software.amazon.lambda.durable.DurableHandler;
8-
import software.amazon.lambda.durable.WaitForConditionConfig;
9-
import software.amazon.lambda.durable.WaitStrategies;
10-
import software.amazon.lambda.durable.retry.JitterStrategy;
7+
import software.amazon.lambda.durable.WaitForConditionResult;
118

129
/**
1310
* Example demonstrating the waitForCondition operation.
1411
*
15-
* <p>Polls a counter until it reaches the length of the input name, then returns the final count.
12+
* <p>Polls a counter until it reaches the length of the input name, then returns the final count. Uses the minimal API
13+
* with default configuration — no explicit strategy or config needed.
1614
*/
1715
public class WaitForConditionExample extends DurableHandler<GreetingRequest, Integer> {
1816

1917
@Override
2018
public Integer handleRequest(GreetingRequest input, DurableContext context) {
2119
var targetCount = input.getName().length();
2220

23-
var strategy = WaitStrategies.<Integer>builder(state -> state < targetCount)
24-
.initialDelay(Duration.ofSeconds(1))
25-
.jitter(JitterStrategy.NONE)
26-
.build();
27-
28-
var config = WaitForConditionConfig.<Integer>builder(strategy, 0).build();
29-
30-
return context.waitForCondition("count-to-name-length", Integer.class, (state, stepCtx) -> state + 1, config);
21+
return context.waitForCondition(
22+
"count-to-name-length",
23+
Integer.class,
24+
(state, stepCtx) -> {
25+
var next = state + 1;
26+
return next >= targetCount
27+
? WaitForConditionResult.stopPolling(next)
28+
: WaitForConditionResult.continuePolling(next);
29+
},
30+
0);
3131
}
3232
}

sdk-integration-tests/src/test/java/software/amazon/lambda/durable/WaitForConditionIntegrationTest.java

Lines changed: 87 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,38 @@
1212
import org.junit.jupiter.api.Test;
1313
import software.amazon.lambda.durable.model.ExecutionStatus;
1414
import software.amazon.lambda.durable.retry.JitterStrategy;
15+
import software.amazon.lambda.durable.retry.WaitForConditionWaitStrategy;
16+
import software.amazon.lambda.durable.retry.WaitStrategies;
1517
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
1618

1719
class WaitForConditionIntegrationTest {
1820

19-
// ---- 5.1: Basic integration tests ----
21+
// ---- Basic integration tests ----
2022

2123
@Test
2224
void testBasicPollingSucceedsAfterNChecks() {
2325
var checkCount = new AtomicInteger(0);
2426
var targetCount = 3;
2527

2628
var runner = LocalDurableTestRunner.create(String.class, (input, ctx) -> {
27-
var strategy = WaitStrategies.<Integer>builder(state -> state < targetCount)
28-
.initialDelay(Duration.ofSeconds(1))
29-
.jitter(JitterStrategy.NONE)
30-
.build();
29+
var strategy = WaitStrategies.<Integer>exponentialBackoff(
30+
60, Duration.ofSeconds(1), Duration.ofSeconds(300), 1.5, JitterStrategy.NONE);
3131

32-
var config = WaitForConditionConfig.<Integer>builder(strategy, 0).build();
32+
var config = WaitForConditionConfig.<Integer>builder()
33+
.waitStrategy(strategy)
34+
.build();
3335

3436
return ctx.waitForCondition(
3537
"poll-counter",
3638
Integer.class,
3739
(state, stepCtx) -> {
3840
checkCount.incrementAndGet();
39-
return state + 1;
41+
var next = state + 1;
42+
return next >= targetCount
43+
? WaitForConditionResult.stopPolling(next)
44+
: WaitForConditionResult.continuePolling(next);
4045
},
46+
0,
4147
config);
4248
});
4349

@@ -51,26 +57,23 @@ void testBasicPollingSucceedsAfterNChecks() {
5157
@Test
5258
void testCustomWaitStrategy() {
5359
var runner = LocalDurableTestRunner.create(String.class, (input, ctx) -> {
54-
// Custom strategy: stop when state equals "done", fixed 2s delay
55-
WaitForConditionWaitStrategy<String> customStrategy = (state, attempt) -> {
56-
if ("done".equals(state)) {
57-
return WaitForConditionDecision.stopPolling();
58-
}
59-
return WaitForConditionDecision.continuePolling(Duration.ofSeconds(2));
60-
};
60+
// Custom strategy: fixed 2s delay
61+
WaitForConditionWaitStrategy<String> customStrategy = (state, attempt) -> Duration.ofSeconds(2);
6162

62-
var config = WaitForConditionConfig.<String>builder(customStrategy, "pending")
63+
var config = WaitForConditionConfig.<String>builder()
64+
.waitStrategy(customStrategy)
6365
.build();
6466

6567
return ctx.waitForCondition(
6668
"custom-strategy",
6769
String.class,
6870
(state, stepCtx) -> {
6971
if ("pending".equals(state)) {
70-
return "processing";
72+
return WaitForConditionResult.continuePolling("processing");
7173
}
72-
return "done";
74+
return WaitForConditionResult.stopPolling("done");
7375
},
76+
"pending",
7477
config);
7578
});
7679

@@ -83,16 +86,19 @@ void testCustomWaitStrategy() {
8386
@Test
8487
void testMaxAttemptsExceeded() {
8588
var runner = LocalDurableTestRunner.create(String.class, (input, ctx) -> {
86-
var strategy = WaitStrategies.<String>builder(state -> true) // always continue
87-
.maxAttempts(3)
88-
.initialDelay(Duration.ofSeconds(1))
89-
.jitter(JitterStrategy.NONE)
90-
.build();
89+
var strategy = WaitStrategies.<String>exponentialBackoff(
90+
3, Duration.ofSeconds(1), Duration.ofSeconds(300), 1.5, JitterStrategy.NONE);
9191

92-
var config =
93-
WaitForConditionConfig.<String>builder(strategy, "initial").build();
92+
var config = WaitForConditionConfig.<String>builder()
93+
.waitStrategy(strategy)
94+
.build();
9495

95-
return ctx.waitForCondition("max-attempts", String.class, (state, stepCtx) -> state, config);
96+
return ctx.waitForCondition(
97+
"max-attempts",
98+
String.class,
99+
(state, stepCtx) -> WaitForConditionResult.continuePolling(state),
100+
"initial",
101+
config);
96102
});
97103

98104
var result = runner.runUntilComplete("test");
@@ -103,20 +109,20 @@ void testMaxAttemptsExceeded() {
103109
@Test
104110
void testCheckFunctionError() {
105111
var runner = LocalDurableTestRunner.create(String.class, (input, ctx) -> {
106-
var strategy = WaitStrategies.<String>builder(state -> true)
107-
.initialDelay(Duration.ofSeconds(1))
108-
.jitter(JitterStrategy.NONE)
109-
.build();
112+
var strategy = WaitStrategies.<String>exponentialBackoff(
113+
60, Duration.ofSeconds(1), Duration.ofSeconds(300), 1.5, JitterStrategy.NONE);
110114

111-
var config =
112-
WaitForConditionConfig.<String>builder(strategy, "initial").build();
115+
var config = WaitForConditionConfig.<String>builder()
116+
.waitStrategy(strategy)
117+
.build();
113118

114119
return ctx.waitForCondition(
115120
"error-check",
116121
String.class,
117122
(state, stepCtx) -> {
118123
throw new IllegalStateException("Check function failed");
119124
},
125+
"initial",
120126
config);
121127
});
122128

@@ -130,20 +136,24 @@ void testReplayAcrossInvocations() {
130136
var checkCount = new AtomicInteger(0);
131137

132138
var runner = LocalDurableTestRunner.create(String.class, (input, ctx) -> {
133-
var strategy = WaitStrategies.<Integer>builder(state -> state < 2)
134-
.initialDelay(Duration.ofSeconds(1))
135-
.jitter(JitterStrategy.NONE)
136-
.build();
139+
var strategy = WaitStrategies.<Integer>exponentialBackoff(
140+
60, Duration.ofSeconds(1), Duration.ofSeconds(300), 1.5, JitterStrategy.NONE);
137141

138-
var config = WaitForConditionConfig.<Integer>builder(strategy, 0).build();
142+
var config = WaitForConditionConfig.<Integer>builder()
143+
.waitStrategy(strategy)
144+
.build();
139145

140146
var result = ctx.waitForCondition(
141147
"replay-poll",
142148
Integer.class,
143149
(state, stepCtx) -> {
144150
checkCount.incrementAndGet();
145-
return state + 1;
151+
var next = state + 1;
152+
return next >= 2
153+
? WaitForConditionResult.stopPolling(next)
154+
: WaitForConditionResult.continuePolling(next);
146155
},
156+
0,
147157
config);
148158

149159
return result.toString();
@@ -162,8 +172,7 @@ void testReplayAcrossInvocations() {
162172
assertEquals(firstCheckCount, checkCount.get());
163173
}
164174

165-
// ---- 5.2: PBT — stopPolling completes with that state as result ----
166-
// **Validates: Requirements 1.5, 2.1**
175+
// ---- PBT — isDone=true completes with that state as result ----
167176

168177
@RepeatedTest(50)
169178
void propertyStopPollingCompletesWithState() {
@@ -172,28 +181,33 @@ void propertyStopPollingCompletesWithState() {
172181
var target = random.nextInt(1, 11);
173182

174183
var runner = LocalDurableTestRunner.create(String.class, (input, ctx) -> {
175-
// Strategy: stop when state reaches target
176-
WaitForConditionWaitStrategy<Integer> strategy = (state, attempt) -> {
177-
if (state >= target) {
178-
return WaitForConditionDecision.stopPolling();
179-
}
180-
return WaitForConditionDecision.continuePolling(Duration.ofSeconds(1));
181-
};
184+
var strategy = WaitStrategies.<Integer>fixedDelay(target + 1, Duration.ofSeconds(1));
182185

183-
var config = WaitForConditionConfig.<Integer>builder(strategy, 0).build();
186+
var config = WaitForConditionConfig.<Integer>builder()
187+
.waitStrategy(strategy)
188+
.build();
184189

185-
return ctx.waitForCondition("stop-polling-prop", Integer.class, (state, stepCtx) -> state + 1, config);
190+
return ctx.waitForCondition(
191+
"stop-polling-prop",
192+
Integer.class,
193+
(state, stepCtx) -> {
194+
var next = state + 1;
195+
return next >= target
196+
? WaitForConditionResult.stopPolling(next)
197+
: WaitForConditionResult.continuePolling(next);
198+
},
199+
0,
200+
config);
186201
});
187202

188203
var result = runner.runUntilComplete("test");
189204

190205
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
191-
// The result should be the state that caused stopPolling — which is target
206+
// The result should be the state that caused isDone=true — which is target
192207
assertEquals(target, result.getResult(Integer.class));
193208
}
194209

195-
// ---- 5.3: PBT — wait strategy receives correct state and attempt ----
196-
// **Validates: Requirements 1.3, 2.1**
210+
// ---- PBT — wait strategy receives correct state and attempt ----
197211

198212
@RepeatedTest(50)
199213
void propertyWaitStrategyReceivesCorrectStateAndAttempt() {
@@ -207,28 +221,39 @@ void propertyWaitStrategyReceivesCorrectStateAndAttempt() {
207221
WaitForConditionWaitStrategy<Integer> strategy = (state, attempt) -> {
208222
observedStates.add(state);
209223
observedAttempts.add(attempt);
210-
if (attempt + 1 >= totalChecks) {
211-
return WaitForConditionDecision.stopPolling();
212-
}
213-
return WaitForConditionDecision.continuePolling(Duration.ofSeconds(1));
224+
return Duration.ofSeconds(1);
214225
};
215226

216-
var config = WaitForConditionConfig.<Integer>builder(strategy, 0).build();
227+
var config = WaitForConditionConfig.<Integer>builder()
228+
.waitStrategy(strategy)
229+
.build();
217230

218-
return ctx.waitForCondition("state-attempt-prop", Integer.class, (state, stepCtx) -> state + 1, config);
231+
return ctx.waitForCondition(
232+
"state-attempt-prop",
233+
Integer.class,
234+
(state, stepCtx) -> {
235+
var next = state + 1;
236+
return next >= totalChecks
237+
? WaitForConditionResult.stopPolling(next)
238+
: WaitForConditionResult.continuePolling(next);
239+
},
240+
0,
241+
config);
219242
});
220243

221244
var result = runner.runUntilComplete("test");
222245

223246
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
224247

225-
// Verify each strategy call received the correct state and attempt
226-
assertEquals(totalChecks, observedStates.size());
227-
assertEquals(totalChecks, observedAttempts.size());
248+
// The strategy is only called when isDone=false, so it's called totalChecks-1 times
249+
// (the last check returns isDone=true, so the strategy is not called)
250+
var expectedStrategyCalls = totalChecks - 1;
251+
assertEquals(expectedStrategyCalls, observedStates.size());
252+
assertEquals(expectedStrategyCalls, observedAttempts.size());
228253

229-
for (int i = 0; i < totalChecks; i++) {
254+
for (int i = 0; i < expectedStrategyCalls; i++) {
230255
// Check function returns state + 1, starting from 0
231-
// So after check i, state = i + 1
256+
// So after check i, state = i + 1, and strategy receives that value
232257
assertEquals(i + 1, observedStates.get(i), "State at strategy call " + (i + 1));
233258
assertEquals(i, observedAttempts.get(i), "Attempt at strategy call " + (i + 1));
234259
}

0 commit comments

Comments
 (0)