Skip to content

Commit 1050e5a

Browse files
committed
Add input validation for duration parameters with 1-second minimum requirement
1 parent eea1738 commit 1050e5a

11 files changed

Lines changed: 416 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.amazonaws.lambda.durable.logging.LoggerConfig;
88
import com.amazonaws.lambda.durable.serde.JacksonSerDes;
99
import com.amazonaws.lambda.durable.serde.SerDes;
10+
import com.amazonaws.lambda.durable.validation.ParameterValidator;
1011
import java.time.Duration;
1112
import java.util.Objects;
1213
import java.util.concurrent.ExecutorService;
@@ -312,6 +313,7 @@ public Builder withLoggerConfig(LoggerConfig loggerConfig) {
312313
* @return This builder
313314
*/
314315
public Builder withPollingInterval(Duration duration) {
316+
ParameterValidator.validateOptionalDuration(duration, "Polling interval");
315317
this.pollingInterval = duration;
316318
return this;
317319
}

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/InvokeConfig.java

Lines changed: 2 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
public class InvokeConfig {
@@ -62,6 +63,7 @@ public Builder tenantId(String tenantId) {
6263
}
6364

6465
public Builder timeout(Duration timeout) {
66+
ParameterValidator.validateOptionalDuration(timeout, "Invoke timeout");
6567
this.timeout = timeout;
6668
return this;
6769
}

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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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: "
34+
+ duration.toSeconds() + " seconds");
35+
}
36+
}
37+
38+
/**
39+
* Validates that an optional duration (if provided) is at least 1 second.
40+
*
41+
* @param duration the duration to validate (can be null)
42+
* @param parameterName the name of the parameter (for error messages)
43+
* @throws IllegalArgumentException if duration is non-null and less than 1 second
44+
*/
45+
public static void validateOptionalDuration(Duration duration, String parameterName) {
46+
if (duration != null && duration.toSeconds() < MIN_DURATION_SECONDS) {
47+
throw new IllegalArgumentException(
48+
parameterName + " must be at least " + MIN_DURATION_SECONDS + " second, got: "
49+
+ duration.toSeconds() + " seconds");
50+
}
51+
}
52+
53+
/**
54+
* Validates that an integer value is positive (greater than 0).
55+
*
56+
* @param value the value to validate
57+
* @param parameterName the name of the parameter (for error messages)
58+
* @throws IllegalArgumentException if value is null or not positive
59+
*/
60+
public static void validatePositiveInteger(Integer value, String parameterName) {
61+
if (value == null) {
62+
throw new IllegalArgumentException(parameterName + " cannot be null");
63+
}
64+
if (value <= 0) {
65+
throw new IllegalArgumentException(parameterName + " must be positive, got: " + value);
66+
}
67+
}
68+
69+
/**
70+
* Validates that an optional integer value (if provided) is positive (greater than 0).
71+
*
72+
* @param value the value to validate (can be null)
73+
* @param parameterName the name of the parameter (for error messages)
74+
* @throws IllegalArgumentException if value is non-null and not positive
75+
*/
76+
public static void validateOptionalPositiveInteger(Integer value, String parameterName) {
77+
if (value != null && value <= 0) {
78+
throw new IllegalArgumentException(parameterName + " must be positive, got: " + value);
79+
}
80+
}
81+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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(
25+
IllegalArgumentException.class,
26+
() -> CallbackConfig.builder().heartbeatTimeout(Duration.ofMillis(999)).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(() -> CallbackConfig.builder()
43+
.timeout(null)
44+
.heartbeatTimeout(null)
45+
.build());
46+
}
47+
48+
@Test
49+
void invokeConfig_withInvalidTimeout_shouldThrow() {
50+
var exception = assertThrows(
51+
IllegalArgumentException.class,
52+
() -> InvokeConfig.builder().timeout(Duration.ofMillis(500)).build());
53+
54+
assertTrue(exception.getMessage().contains("Invoke timeout"));
55+
assertTrue(exception.getMessage().contains("at least 1 second"));
56+
}
57+
58+
@Test
59+
void invokeConfig_withValidTimeout_shouldPass() {
60+
assertDoesNotThrow(() -> InvokeConfig.builder().timeout(Duration.ofSeconds(30)).build());
61+
}
62+
63+
@Test
64+
void invokeConfig_withNullTimeout_shouldPass() {
65+
assertDoesNotThrow(() -> InvokeConfig.builder().timeout(null).build());
66+
}
67+
68+
@Test
69+
void durableConfig_withInvalidPollingInterval_shouldThrow() {
70+
var exception = assertThrows(
71+
IllegalArgumentException.class,
72+
() -> DurableConfig.builder().withPollingInterval(Duration.ofMillis(500)).build());
73+
74+
assertTrue(exception.getMessage().contains("Polling interval"));
75+
assertTrue(exception.getMessage().contains("at least 1 second"));
76+
}
77+
78+
@Test
79+
void durableConfig_withValidPollingInterval_shouldPass() {
80+
assertDoesNotThrow(() -> DurableConfig.builder()
81+
.withPollingInterval(Duration.ofSeconds(2))
82+
.build());
83+
}
84+
85+
@Test
86+
void durableConfig_withNullPollingInterval_shouldPass() {
87+
assertDoesNotThrow(() -> DurableConfig.builder()
88+
.withPollingInterval(null)
89+
.build());
90+
}
91+
}

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);

0 commit comments

Comments
 (0)