Skip to content

Commit b5a80d8

Browse files
authored
Add duration validation (#109)
* Add input validation for duration parameters with 1-second minimum requirement * Add input validation for duration parameters with 1-second minimum requirement * Add input validation for duration parameters with 1-second minimum requirement * Add input validation for duration parameters with 1-second minimum requirement * Add input validation for duration parameters with 1-second minimum requirement
1 parent eea1738 commit b5a80d8

10 files changed

Lines changed: 364 additions & 11 deletions

File tree

sdk/src/main/java/com/amazonaws/lambda/durable/CallbackConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package com.amazonaws.lambda.durable;
44

55
import com.amazonaws.lambda.durable.serde.SerDes;
6+
import com.amazonaws.lambda.durable.validation.ParameterValidator;
67
import java.time.Duration;
78

89
/** Configuration for callback operations. */
@@ -60,11 +61,13 @@ private Builder(Duration timeout, Duration heartbeatTimeout, SerDes serDes) {
6061
}
6162

6263
public Builder timeout(Duration timeout) {
64+
ParameterValidator.validateOptionalDuration(timeout, "Callback timeout");
6365
this.timeout = timeout;
6466
return this;
6567
}
6668

6769
public Builder heartbeatTimeout(Duration heartbeatTimeout) {
70+
ParameterValidator.validateOptionalDuration(heartbeatTimeout, "Heartbeat timeout");
6871
this.heartbeatTimeout = heartbeatTimeout;
6972
return this;
7073
}

sdk/src/main/java/com/amazonaws/lambda/durable/DurableConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ public Builder withLoggerConfig(LoggerConfig loggerConfig) {
312312
* @return This builder
313313
*/
314314
public Builder withPollingInterval(Duration duration) {
315+
// No validation - polling intervals can be less than 1 second (e.g., 200ms with backoff)
315316
this.pollingInterval = duration;
316317
return this;
317318
}

sdk/src/main/java/com/amazonaws/lambda/durable/DurableContext.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.amazonaws.lambda.durable.operation.InvokeOperation;
1010
import com.amazonaws.lambda.durable.operation.StepOperation;
1111
import com.amazonaws.lambda.durable.operation.WaitOperation;
12+
import com.amazonaws.lambda.durable.validation.ParameterValidator;
1213
import com.amazonaws.services.lambda.runtime.Context;
1314
import java.time.Duration;
1415
import java.util.Objects;
@@ -107,6 +108,7 @@ public Void wait(Duration duration) {
107108
}
108109

109110
public Void wait(String waitName, Duration duration) {
111+
ParameterValidator.validateDuration(duration, "Wait duration");
110112
var operationId = nextOperationId();
111113

112114
// Create and start wait operation

sdk/src/main/java/com/amazonaws/lambda/durable/operation/WaitOperation.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.amazonaws.lambda.durable.execution.ExecutionManager;
77
import com.amazonaws.lambda.durable.serde.NoopSerDes;
88
import com.amazonaws.lambda.durable.serde.SerDes;
9+
import com.amazonaws.lambda.durable.validation.ParameterValidator;
910
import java.time.Duration;
1011
import java.time.Instant;
1112
import org.slf4j.Logger;
@@ -25,6 +26,7 @@ public class WaitOperation extends BaseDurableOperation<Void> {
2526

2627
public WaitOperation(String operationId, String name, Duration duration, ExecutionManager executionManager) {
2728
super(operationId, name, OperationType.WAIT, TypeToken.get(Void.class), NOOP_SER_DES, executionManager);
29+
ParameterValidator.validateDuration(duration, "Wait duration");
2830
this.duration = duration;
2931
}
3032

sdk/src/main/java/com/amazonaws/lambda/durable/retry/RetryStrategies.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package com.amazonaws.lambda.durable.retry;
44

5+
import com.amazonaws.lambda.durable.validation.ParameterValidator;
56
import java.time.Duration;
67

78
/**
@@ -49,12 +50,8 @@ public static RetryStrategy exponentialBackoff(
4950
if (maxAttempts <= 0) {
5051
throw new IllegalArgumentException("maxAttempts must be positive");
5152
}
52-
if (initialDelay.isNegative()) {
53-
throw new IllegalArgumentException("initialDelay must not be negative");
54-
}
55-
if (maxDelay.isNegative()) {
56-
throw new IllegalArgumentException("maxDelay must not be negative");
57-
}
53+
ParameterValidator.validateDuration(initialDelay, "initialDelay");
54+
ParameterValidator.validateDuration(maxDelay, "maxDelay");
5855
if (backoffRate <= 0) {
5956
throw new IllegalArgumentException("backoffRate must be positive");
6057
}
@@ -98,9 +95,7 @@ public static RetryStrategy fixedDelay(int maxAttempts, Duration fixedDelay) {
9895
if (maxAttempts <= 0) {
9996
throw new IllegalArgumentException("maxAttempts must be positive");
10097
}
101-
if (fixedDelay.isNegative()) {
102-
throw new IllegalArgumentException("fixedDelay must not be negative");
103-
}
98+
ParameterValidator.validateDuration(fixedDelay, "fixedDelay");
10499

105100
return (error, attemptNumber) -> {
106101
if (attemptNumber + 1 >= maxAttempts) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazonaws.lambda.durable.validation;
4+
5+
import java.time.Duration;
6+
7+
/**
8+
* Utility class for validating input parameters in the Durable Execution SDK.
9+
*
10+
* <p>Provides common validation methods to ensure consistent error messages and validation logic across the SDK.
11+
*/
12+
public final class ParameterValidator {
13+
14+
private static final long MIN_DURATION_SECONDS = 1;
15+
16+
private ParameterValidator() {
17+
// Utility class - prevent instantiation
18+
}
19+
20+
/**
21+
* Validates that a duration is at least 1 second.
22+
*
23+
* @param duration the duration to validate
24+
* @param parameterName the name of the parameter (for error messages)
25+
* @throws IllegalArgumentException if duration is null or less than 1 second
26+
*/
27+
public static void validateDuration(Duration duration, String parameterName) {
28+
if (duration == null) {
29+
throw new IllegalArgumentException(parameterName + " cannot be null");
30+
}
31+
if (duration.toSeconds() < MIN_DURATION_SECONDS) {
32+
throw new IllegalArgumentException(
33+
parameterName + " must be at least " + MIN_DURATION_SECONDS + " second, got: " + duration);
34+
}
35+
}
36+
37+
/**
38+
* Validates that an optional duration (if provided) is at least 1 second.
39+
*
40+
* @param duration the duration to validate (can be null)
41+
* @param parameterName the name of the parameter (for error messages)
42+
* @throws IllegalArgumentException if duration is non-null and less than 1 second
43+
*/
44+
public static void validateOptionalDuration(Duration duration, String parameterName) {
45+
if (duration != null && duration.toSeconds() < MIN_DURATION_SECONDS) {
46+
throw new IllegalArgumentException(
47+
parameterName + " must be at least " + MIN_DURATION_SECONDS + " second, got: " + duration);
48+
}
49+
}
50+
51+
/**
52+
* Validates that an integer value is positive (greater than 0).
53+
*
54+
* @param value the value to validate
55+
* @param parameterName the name of the parameter (for error messages)
56+
* @throws IllegalArgumentException if value is null or not positive
57+
*/
58+
public static void validatePositiveInteger(Integer value, String parameterName) {
59+
if (value == null) {
60+
throw new IllegalArgumentException(parameterName + " cannot be null");
61+
}
62+
if (value <= 0) {
63+
throw new IllegalArgumentException(parameterName + " must be positive, got: " + value);
64+
}
65+
}
66+
67+
/**
68+
* Validates that an optional integer value (if provided) is positive (greater than 0).
69+
*
70+
* @param value the value to validate (can be null)
71+
* @param parameterName the name of the parameter (for error messages)
72+
* @throws IllegalArgumentException if value is non-null and not positive
73+
*/
74+
public static void validateOptionalPositiveInteger(Integer value, String parameterName) {
75+
if (value != null && value <= 0) {
76+
throw new IllegalArgumentException(parameterName + " must be positive, got: " + value);
77+
}
78+
}
79+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazonaws.lambda.durable;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
import java.time.Duration;
8+
import org.junit.jupiter.api.Test;
9+
10+
class DurationValidationIntegrationTest {
11+
12+
@Test
13+
void callbackConfig_withInvalidTimeout_shouldThrow() {
14+
var exception = assertThrows(
15+
IllegalArgumentException.class,
16+
() -> CallbackConfig.builder().timeout(Duration.ofMillis(500)).build());
17+
18+
assertTrue(exception.getMessage().contains("Callback timeout"));
19+
assertTrue(exception.getMessage().contains("at least 1 second"));
20+
}
21+
22+
@Test
23+
void callbackConfig_withInvalidHeartbeatTimeout_shouldThrow() {
24+
var exception = assertThrows(IllegalArgumentException.class, () -> CallbackConfig.builder()
25+
.heartbeatTimeout(Duration.ofMillis(999))
26+
.build());
27+
28+
assertTrue(exception.getMessage().contains("Heartbeat timeout"));
29+
assertTrue(exception.getMessage().contains("at least 1 second"));
30+
}
31+
32+
@Test
33+
void callbackConfig_withValidTimeouts_shouldPass() {
34+
assertDoesNotThrow(() -> CallbackConfig.builder()
35+
.timeout(Duration.ofSeconds(30))
36+
.heartbeatTimeout(Duration.ofSeconds(10))
37+
.build());
38+
}
39+
40+
@Test
41+
void callbackConfig_withNullTimeouts_shouldPass() {
42+
assertDoesNotThrow(() ->
43+
CallbackConfig.builder().timeout(null).heartbeatTimeout(null).build());
44+
}
45+
}

sdk/src/test/java/com/amazonaws/lambda/durable/operation/WaitOperationTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.junit.jupiter.api.Assertions.assertEquals;
66
import static org.junit.jupiter.api.Assertions.assertNull;
77
import static org.junit.jupiter.api.Assertions.assertThrows;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
89
import static org.mockito.Mockito.any;
910
import static org.mockito.Mockito.mock;
1011
import static org.mockito.Mockito.when;
@@ -21,6 +22,49 @@
2122

2223
class WaitOperationTest {
2324

25+
@Test
26+
void constructor_withNullDuration_shouldThrow() {
27+
var executionManager = mock(ExecutionManager.class);
28+
29+
var exception = assertThrows(
30+
IllegalArgumentException.class, () -> new WaitOperation("1", "test-wait", null, executionManager));
31+
32+
assertEquals("Wait duration cannot be null", exception.getMessage());
33+
}
34+
35+
@Test
36+
void constructor_withZeroDuration_shouldThrow() {
37+
var executionManager = mock(ExecutionManager.class);
38+
39+
var exception = assertThrows(
40+
IllegalArgumentException.class,
41+
() -> new WaitOperation("1", "test-wait", Duration.ofSeconds(0), executionManager));
42+
43+
assertTrue(exception.getMessage().contains("Wait duration"));
44+
assertTrue(exception.getMessage().contains("at least 1 second"));
45+
}
46+
47+
@Test
48+
void constructor_withSubSecondDuration_shouldThrow() {
49+
var executionManager = mock(ExecutionManager.class);
50+
51+
var exception = assertThrows(
52+
IllegalArgumentException.class,
53+
() -> new WaitOperation("1", "test-wait", Duration.ofMillis(500), executionManager));
54+
55+
assertTrue(exception.getMessage().contains("Wait duration"));
56+
assertTrue(exception.getMessage().contains("at least 1 second"));
57+
}
58+
59+
@Test
60+
void constructor_withValidDuration_shouldPass() {
61+
var executionManager = mock(ExecutionManager.class);
62+
63+
var operation = new WaitOperation("1", "test-wait", Duration.ofSeconds(10), executionManager);
64+
65+
assertEquals("1", operation.getOperationId());
66+
}
67+
2468
@Test
2569
void getThrowsIllegalStateExceptionWhenCalledFromStepContext() {
2670
var executionManager = mock(ExecutionManager.class);

sdk/src/test/java/com/amazonaws/lambda/durable/retry/RetryStrategiesTest.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ void testJitterStrategies() {
112112

113113
@Test
114114
void testMinimumDelayOfOneSecond() {
115-
// Test with very small initial delay to verify 1-second minimum
115+
// Test that delays are properly calculated with 1-second minimum
116116
var strategy = RetryStrategies.exponentialBackoff(
117-
5, Duration.ofMillis(100), Duration.ofSeconds(60), 1.0, JitterStrategy.FULL);
117+
5, Duration.ofSeconds(1), Duration.ofSeconds(60), 1.0, JitterStrategy.FULL);
118118

119119
var decision = strategy.makeRetryDecision(new RuntimeException("test"), 0);
120120
assertTrue(decision.delay().toSeconds() >= 1, "Delay should be at least 1 second");
@@ -165,6 +165,65 @@ void testInvalidParameters() {
165165
assertThrows(IllegalArgumentException.class, () -> RetryStrategies.fixedDelay(5, Duration.ofSeconds(-1)));
166166
}
167167

168+
@Test
169+
void exponentialBackoff_withSubSecondInitialDelay_shouldThrow() {
170+
var exception = assertThrows(
171+
IllegalArgumentException.class,
172+
() -> RetryStrategies.exponentialBackoff(
173+
3, Duration.ofMillis(500), Duration.ofSeconds(60), 2.0, JitterStrategy.NONE));
174+
175+
assertTrue(exception.getMessage().contains("initialDelay"));
176+
assertTrue(exception.getMessage().contains("at least 1 second"));
177+
}
178+
179+
@Test
180+
void exponentialBackoff_withSubSecondMaxDelay_shouldThrow() {
181+
var exception = assertThrows(
182+
IllegalArgumentException.class,
183+
() -> RetryStrategies.exponentialBackoff(
184+
3, Duration.ofSeconds(5), Duration.ofMillis(999), 2.0, JitterStrategy.NONE));
185+
186+
assertTrue(exception.getMessage().contains("maxDelay"));
187+
assertTrue(exception.getMessage().contains("at least 1 second"));
188+
}
189+
190+
@Test
191+
void exponentialBackoff_withNullInitialDelay_shouldThrow() {
192+
var exception = assertThrows(
193+
IllegalArgumentException.class,
194+
() -> RetryStrategies.exponentialBackoff(3, null, Duration.ofSeconds(60), 2.0, JitterStrategy.NONE));
195+
196+
assertTrue(exception.getMessage().contains("initialDelay"));
197+
assertTrue(exception.getMessage().contains("cannot be null"));
198+
}
199+
200+
@Test
201+
void exponentialBackoff_withNullMaxDelay_shouldThrow() {
202+
var exception = assertThrows(
203+
IllegalArgumentException.class,
204+
() -> RetryStrategies.exponentialBackoff(3, Duration.ofSeconds(5), null, 2.0, JitterStrategy.NONE));
205+
206+
assertTrue(exception.getMessage().contains("maxDelay"));
207+
assertTrue(exception.getMessage().contains("cannot be null"));
208+
}
209+
210+
@Test
211+
void fixedDelay_withSubSecondDelay_shouldThrow() {
212+
var exception = assertThrows(
213+
IllegalArgumentException.class, () -> RetryStrategies.fixedDelay(3, Duration.ofMillis(500)));
214+
215+
assertTrue(exception.getMessage().contains("fixedDelay"));
216+
assertTrue(exception.getMessage().contains("at least 1 second"));
217+
}
218+
219+
@Test
220+
void fixedDelay_withNullDelay_shouldThrow() {
221+
var exception = assertThrows(IllegalArgumentException.class, () -> RetryStrategies.fixedDelay(3, null));
222+
223+
assertTrue(exception.getMessage().contains("fixedDelay"));
224+
assertTrue(exception.getMessage().contains("cannot be null"));
225+
}
226+
168227
@Test
169228
void testStepConfigWithRetryStrategy() {
170229
var config1 = StepConfig.builder()

0 commit comments

Comments
 (0)