diff --git a/multicloudj-common/pom.xml b/multicloudj-common/pom.xml index d7b17a51..f231b213 100644 --- a/multicloudj-common/pom.xml +++ b/multicloudj-common/pom.xml @@ -21,6 +21,12 @@ 5.12.1 test + + org.projectlombok + lombok + 1.18.34 + provided + org.wiremock wiremock diff --git a/multicloudj-common/src/main/java/com/salesforce/multicloudj/common/retries/RetryConfig.java b/multicloudj-common/src/main/java/com/salesforce/multicloudj/common/retries/RetryConfig.java new file mode 100644 index 00000000..9a147a84 --- /dev/null +++ b/multicloudj-common/src/main/java/com/salesforce/multicloudj/common/retries/RetryConfig.java @@ -0,0 +1,107 @@ +package com.salesforce.multicloudj.common.retries; + +import lombok.Builder; +import lombok.Getter; + +/** + * Cloud-agnostic retry configuration for MultiCloudJ services. + * + *

This configuration provides a unified retry strategy that can be applied across all supported + * cloud providers and services. + * It abstracts provider-specific retry mechanisms into a common model that can be translated to + * native retry configurations for each cloud provider. + * + *

Supports two retry modes: + *

+ * + *

Example usage: + *

{@code
+ * // Exponential backoff with 3 retries
+ * RetryConfig exponentialConfig = RetryConfig.builder()
+ *     .mode(RetryConfig.Mode.EXPONENTIAL)
+ *     .maxAttempts(3)
+ *     .initialDelayMillis(100L)
+ *     .multiplier(2.0)
+ *     .maxDelayMillis(5000L)
+ *     .totalTimeoutMillis(30000L)
+ *     .build();
+ *
+ * // Fixed delay with 5 retries
+ * RetryConfig fixedConfig = RetryConfig.builder()
+ *     .mode(RetryConfig.Mode.FIXED)
+ *     .maxAttempts(5)
+ *     .fixedDelayMillis(1000L)
+ *     .build();
+ * }
+ */ +@Getter +@Builder +public final class RetryConfig { + /** + * Retry mode determining the delay calculation strategy between retry attempts. + */ + public enum Mode { + /** + * Exponential backoff mode where delay grows exponentially between retries. + * Uses formula: {@code delay = min(maxDelayMillis, initialDelayMillis * multiplier^(attempt-1))} + * + *

Requires: {@code initialDelayMillis}, {@code multiplier}, {@code maxDelayMillis} + */ + EXPONENTIAL, + + /** + * Fixed delay mode where the same delay is used between all retry attempts. + * + *

Requires: {@code fixedDelayMillis} + */ + FIXED, + } + + /** + * The retry mode determining delay calculation strategy. + */ + private final Mode mode; + + /** + * Maximum number of attempts including the initial request. + * For example, {@code maxAttempts = 3} means 1 initial attempt + 2 retries. + */ + private final int maxAttempts; + + /** + * Initial delay in milliseconds before the first retry (EXPONENTIAL mode only). + * This is the base delay that gets multiplied by {@code multiplier^(attempt-1)}. + */ + private final long initialDelayMillis; + + /** + * Multiplier for exponential backoff (EXPONENTIAL mode only). + * Each retry delay is calculated as: {@code initialDelayMillis * multiplier^(attempt-1)}. + * Common values: 2.0 for doubling delay, + * Default will be 2.0 if you don't specific it. AWS is always 2.0 + */ + private final double multiplier; + + /** + * Maximum delay cap in milliseconds (EXPONENTIAL mode only). + * Prevents delay from growing indefinitely by capping it at this value. + */ + private final long maxDelayMillis; + + /** + * Fixed delay in milliseconds between retries (FIXED mode only). + * The same delay is used for all retry attempts. + */ + private final long fixedDelayMillis; + + /** + * Optional total timeout in milliseconds for all retry attempts combined. + * If set, the retry logic will stop retrying once this timeout is exceeded, + * even if {@code maxAttempts} has not been reached. + */ + private final Long totalTimeoutMillis; +} diff --git a/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/retries/RetryConfigTest.java b/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/retries/RetryConfigTest.java new file mode 100644 index 00000000..52e71419 --- /dev/null +++ b/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/retries/RetryConfigTest.java @@ -0,0 +1,101 @@ +package com.salesforce.multicloudj.common.retries; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class RetryConfigTest { + + @Test + void testExponentialModeBuilder() { + RetryConfig config = RetryConfig.builder() + .mode(RetryConfig.Mode.EXPONENTIAL) + .maxAttempts(5) + .initialDelayMillis(100L) + .multiplier(2.0) + .maxDelayMillis(5000L) + .totalTimeoutMillis(30000L) + .build(); + + assertEquals(RetryConfig.Mode.EXPONENTIAL, config.getMode()); + assertEquals(5, config.getMaxAttempts()); + assertEquals(100L, config.getInitialDelayMillis()); + assertEquals(2.0, config.getMultiplier()); + assertEquals(5000L, config.getMaxDelayMillis()); + assertEquals(30000L, config.getTotalTimeoutMillis()); + } + + @Test + void testFixedModeBuilder() { + RetryConfig config = RetryConfig.builder() + .mode(RetryConfig.Mode.FIXED) + .maxAttempts(3) + .fixedDelayMillis(1000L) + .totalTimeoutMillis(10000L) + .build(); + + assertEquals(RetryConfig.Mode.FIXED, config.getMode()); + assertEquals(3, config.getMaxAttempts()); + assertEquals(1000L, config.getFixedDelayMillis()); + assertEquals(10000L, config.getTotalTimeoutMillis()); + } + + @Test + void testBuilderWithoutTotalTimeout() { + RetryConfig config = RetryConfig.builder() + .mode(RetryConfig.Mode.EXPONENTIAL) + .maxAttempts(3) + .initialDelayMillis(50L) + .multiplier(1.5) + .maxDelayMillis(2000L) + .build(); + + assertEquals(RetryConfig.Mode.EXPONENTIAL, config.getMode()); + assertEquals(3, config.getMaxAttempts()); + assertEquals(50L, config.getInitialDelayMillis()); + assertEquals(1.5, config.getMultiplier()); + assertEquals(2000L, config.getMaxDelayMillis()); + assertNull(config.getTotalTimeoutMillis()); + } + + @Test + void testMinimalExponentialConfig() { + RetryConfig config = RetryConfig.builder() + .mode(RetryConfig.Mode.EXPONENTIAL) + .maxAttempts(1) + .initialDelayMillis(10L) + .multiplier(1.0) + .maxDelayMillis(10L) + .build(); + + assertNotNull(config); + assertEquals(RetryConfig.Mode.EXPONENTIAL, config.getMode()); + assertEquals(1, config.getMaxAttempts()); + assertEquals(10L, config.getInitialDelayMillis()); + assertEquals(1.0, config.getMultiplier()); + assertEquals(10L, config.getMaxDelayMillis()); + } + + @Test + void testMinimalFixedConfig() { + RetryConfig config = RetryConfig.builder() + .mode(RetryConfig.Mode.FIXED) + .maxAttempts(1) + .fixedDelayMillis(100L) + .build(); + + assertNotNull(config); + assertEquals(RetryConfig.Mode.FIXED, config.getMode()); + assertEquals(1, config.getMaxAttempts()); + assertEquals(100L, config.getFixedDelayMillis()); + } + + @Test + void testModeEnum() { + assertEquals(2, RetryConfig.Mode.values().length); + assertEquals(RetryConfig.Mode.EXPONENTIAL, RetryConfig.Mode.valueOf("EXPONENTIAL")); + assertEquals(RetryConfig.Mode.FIXED, RetryConfig.Mode.valueOf("FIXED")); + } +}