BatchWriteRetryConfig.java

/*
 * Copyright © 2018 spring-data-dynamodb (https://github.com/prasanna0586/spring-data-dynamodb)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.socialsignin.spring.data.dynamodb.core;

import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import java.util.Random;

/**
 * Configuration for batch write operation retry behavior.
 *
 * AWS recommends using exponential backoff when retrying batch operations with unprocessed items.
 * This configuration allows customization of the retry strategy while providing sensible defaults
 * based on AWS best practices.
 * @see <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html">
 *      AWS DynamoDB Error Handling</a>
 */
public class BatchWriteRetryConfig {

    /**
     * Default maximum number of retry attempts for batch operations.
     * DynamoDB clients use a default maximum retry count of 8 (AWS SDK for Java 2.x).
     */
    public static final int DEFAULT_MAX_RETRIES = 8;

    /**
     * Default base delay in milliseconds before the first retry.
     * <p>
     * AWS SDK for Java 2.x uses 100ms for non-throttling exceptions.
     * Unprocessed items in batch operations are typically throttling-related,
     * but we use 100ms as a conservative starting point.
     */
    public static final long DEFAULT_BASE_DELAY_MS = 100L;

    /**
     * Default maximum delay in milliseconds between retries.
     * <p>
     * AWS SDK for Java 2.x uses 20 seconds as the maximum delay.
     */
    public static final long DEFAULT_MAX_DELAY_MS = 20000L; // 20 seconds

    /**
     * Default setting for jitter.
     * <p>
     * AWS recommends using jitter to prevent thundering herd problem.
     */
    public static final boolean DEFAULT_USE_JITTER = true;

    private final int maxRetries;
    private final long baseDelayMs;
    private final long maxDelayMs;
    private final boolean useJitter;
    @Nullable
    private final Random random;

    /**
     * Creates a default retry configuration with AWS SDK for Java 2.x settings:
     * - Max retries: 8 (DynamoDB default)
     * - Base delay: 100ms (doubles with each retry: 100, 200, 400, 800, 1600...)
     * - Max delay: 20 seconds
     * - Jitter enabled
     */
    public BatchWriteRetryConfig() {
        this(DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY_MS, DEFAULT_MAX_DELAY_MS, DEFAULT_USE_JITTER);
    }

    /**
     * Creates a custom retry configuration.
     * @param maxRetries Maximum number of retry attempts (must be >= 0)
     * @param baseDelayMs Base delay in milliseconds before first retry (must be > 0)
     * @param maxDelayMs Maximum delay in milliseconds between retries (must be >= baseDelayMs)
     * @param useJitter Whether to add random jitter to delays
     */
    public BatchWriteRetryConfig(int maxRetries, long baseDelayMs, long maxDelayMs, boolean useJitter) {
        if (maxRetries < 0) {
            throw new IllegalArgumentException("maxRetries must be >= 0");
        }
        if (baseDelayMs <= 0) {
            throw new IllegalArgumentException("baseDelayMs must be > 0");
        }
        if (maxDelayMs < baseDelayMs) {
            throw new IllegalArgumentException("maxDelayMs must be >= baseDelayMs");
        }

        this.maxRetries = maxRetries;
        this.baseDelayMs = baseDelayMs;
        this.maxDelayMs = maxDelayMs;
        this.useJitter = useJitter;
        this.random = useJitter ? new Random() : null;
    }

    /**
     * Calculates the delay before the next retry using exponential backoff.
     * <p>
     * Formula: min(baseDelay * 2^retryCount, maxDelay)
     * <p>
     * With jitter: delay * (0.5 + random(0, 0.5))
     * @param retryCount The current retry attempt (0-based)
     * @return Delay in milliseconds before the next retry
     */
    public long getDelayBeforeRetry(int retryCount) {
        if (retryCount < 0) {
            throw new IllegalArgumentException("retryCount must be >= 0");
        }

        // Calculate exponential backoff: baseDelay * 2^retryCount
        long delay = baseDelayMs;
        for (int i = 0; i < retryCount; i++) {
            delay *= 2;
            if (delay >= maxDelayMs) {
                delay = maxDelayMs;
                break;
            }
        }

        // Cap at maximum delay
        delay = Math.min(delay, maxDelayMs);

        // Add jitter if enabled (randomize between 50% and 100% of calculated delay)
        if (useJitter && random != null) {
            double jitterFactor = 0.5 + (random.nextDouble() * 0.5);
            delay = (long) (delay * jitterFactor);
        }

        return delay;
    }

    /**
     * Gets the maximum number of retry attempts.
     * @return The maximum number of retries
     */
    public int getMaxRetries() {
        return maxRetries;
    }

    /**
     * Gets the base delay in milliseconds before the first retry.
     * @return The base delay in milliseconds
     */
    public long getBaseDelayMs() {
        return baseDelayMs;
    }

    /**
     * Gets the maximum delay in milliseconds between retries.
     * @return The maximum delay in milliseconds
     */
    public long getMaxDelayMs() {
        return maxDelayMs;
    }

    /**
     * Whether jitter is enabled for retry delays.
     * @return True if jitter is enabled, false otherwise
     */
    public boolean isUseJitter() {
        return useJitter;
    }

    /**
     * Builder for creating custom BatchWriteRetryConfig instances.
     * <p>
     * Uses fluent builder pattern to customize batch write retry configuration.
     * All configuration values have sensible defaults based on AWS SDK for Java 2.x.
     * <p>
     * Example usage:
     * <pre>
     * BatchWriteRetryConfig config = new BatchWriteRetryConfig.Builder()
     *     .maxRetries(5)
     *     .baseDelayMs(200)
     *     .maxDelayMs(10000)
     *     .useJitter(true)
     *     .build();
     * </pre>
     */
    public static class Builder {
        /**
         * Creates a new Builder with default retry configuration values.
         */
        public Builder() {
        }

        private int maxRetries = DEFAULT_MAX_RETRIES;
        private long baseDelayMs = DEFAULT_BASE_DELAY_MS;
        private long maxDelayMs = DEFAULT_MAX_DELAY_MS;
        private boolean useJitter = DEFAULT_USE_JITTER;

        /**
         * Sets the maximum number of retry attempts.
         * @param maxRetries Maximum number of retries (must be >= 0)
         * @return This builder instance for method chaining
         */
        @NonNull
        public Builder maxRetries(int maxRetries) {
            this.maxRetries = maxRetries;
            return this;
        }

        /**
         * Sets the base delay in milliseconds before the first retry.
         * @param baseDelayMs Base delay in milliseconds (must be > 0)
         * @return This builder instance for method chaining
         */
        @NonNull
        public Builder baseDelayMs(long baseDelayMs) {
            this.baseDelayMs = baseDelayMs;
            return this;
        }

        /**
         * Sets the maximum delay in milliseconds between retries.
         * @param maxDelayMs Maximum delay in milliseconds (must be >= baseDelayMs)
         * @return This builder instance for method chaining
         */
        @NonNull
        public Builder maxDelayMs(long maxDelayMs) {
            this.maxDelayMs = maxDelayMs;
            return this;
        }

        /**
         * Sets whether to add random jitter to retry delays.
         * @param useJitter True to enable jitter, false to disable
         * @return This builder instance for method chaining
         */
        @NonNull
        public Builder useJitter(boolean useJitter) {
            this.useJitter = useJitter;
            return this;
        }

        /**
         * Disables retry logic completely by setting maxRetries to 0.
         * @return This builder instance for method chaining
         */
        @NonNull
        public Builder disableRetries() {
            this.maxRetries = 0;
            return this;
        }

        /**
         * Builds and returns a new BatchWriteRetryConfig instance with the configured settings.
         * @return A new BatchWriteRetryConfig instance
         * @throws IllegalArgumentException if the configuration is invalid
         */
        @NonNull
        public BatchWriteRetryConfig build() {
            return new BatchWriteRetryConfig(maxRetries, baseDelayMs, maxDelayMs, useJitter);
        }
    }

    @NonNull
    @Override
    public String toString() {
        return "BatchWriteRetryConfig{" +
                "maxRetries=" + maxRetries +
                ", baseDelayMs=" + baseDelayMs +
                ", maxDelayMs=" + maxDelayMs +
                ", useJitter=" + useJitter +
                '}';
    }
}