ExceptionHandler.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.utils;

import org.socialsignin.spring.data.dynamodb.exception.BatchDeleteException;
import org.socialsignin.spring.data.dynamodb.exception.BatchWriteException;
import org.springframework.dao.DataAccessException;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteResult;

import java.util.List;

/**
 * Interface for handling exceptions in batch operations (write and delete).
 *
 * This interface provides methods to convert SDK v2 batch operation failures
 * into rich exception objects that expose unprocessed entities for consumer handling.
 */
public interface ExceptionHandler {

    /**
     * Repackages batch operation failures into a rich exception with unprocessed entity information.
     * <p>
     * This method is called after retry logic has been exhausted. It creates an exception that:
     * - Exposes unprocessed entities for custom recovery logic
     * - Includes retry attempt count
     * - Preserves any thrown exceptions
     * <p>
     * Following AWS SDK v2 best practices, unprocessed items are returned to the caller
     * for custom handling (e.g., dead letter queue, custom retry, alerting).
     * <p>
     * Supports both batch write and batch delete operations.
     * @param unprocessedEntities List of entity objects that could not be written/deleted after retries
     * @param retriesAttempted Number of retry attempts that were made
     * @param cause Original exception if one was thrown, or null if items were just unprocessed
     * @param targetType The exception class to instantiate (BatchWriteException or BatchDeleteException)
     * @param <T> Exception type extending DataAccessException
     * @return Exception instance with full failure context
     */
    @NonNull
    default <T extends DataAccessException> T repackageToException(
            @NonNull List<Object> unprocessedEntities,
            int retriesAttempted,
            @Nullable Throwable cause,
            @NonNull Class<T> targetType) {

        // SDK v2: After retry exhaustion, expose unprocessed items for consumer handling
        // Unprocessed items typically indicate persistent throttling, capacity limits, or transaction conflicts

        // Determine operation type based on exception class
        String operationType = targetType == BatchDeleteException.class ? "delete" : "write";

        String message;
        if (cause != null) {
            // An actual exception was thrown
            message = String.format(
                    "Batch %s operation failed with exception: %s. %d entities could not be processed.",
                    operationType,
                    cause.getClass().getSimpleName(),
                    unprocessedEntities.size());
        } else {
            // Items remained unprocessed after retries
            message = String.format(
                    "Batch %s operation failed with %d unprocessed entities after %d retry attempts. " +
                    "Items were not processed despite exponential backoff, likely due to persistent throttling, " +
                    "insufficient provisioned capacity, or transaction conflicts. " +
                    "Consider increasing table provisioned throughput or adjusting retry configuration.",
                    operationType,
                    unprocessedEntities.size(),
                    retriesAttempted);
        }

        if (targetType == BatchWriteException.class) {
            @SuppressWarnings("unchecked")
            T exception = (T) new BatchWriteException(message, unprocessedEntities, retriesAttempted, cause);
            return exception;
        } else if (targetType == BatchDeleteException.class) {
            @SuppressWarnings("unchecked")
            T exception = (T) new BatchDeleteException(message, unprocessedEntities, retriesAttempted, cause);
            return exception;
        } else {
            throw new IllegalArgumentException(
                    "targetType must be BatchWriteException or BatchDeleteException for SDK v2. Received: " + targetType.getName());
        }
    }

    /**
     * Legacy method signature maintained for internal compatibility during migration.
     * This will be removed once DynamoDBTemplate is fully migrated to SDK v2.
     * <p>
     * Converts failed batch results into a rich exception with unprocessed entity information.
     * @param <T> Exception type extending DataAccessException
     * @param failedBatches List of batch results containing unprocessed items
     * @param targetType The exception class to instantiate (BatchWriteException or BatchDeleteException)
     * @return Exception instance with failure context
     * @deprecated Use {@link #repackageToException(List, int, Throwable, Class)} instead
     */
    @NonNull
    @Deprecated
    default <T extends DataAccessException> T repackageToException(
            @NonNull List<BatchWriteResult> failedBatches,
            @NonNull Class<T> targetType) {

        // Temporary implementation until DynamoDBTemplate is migrated
        // Cannot extract actual unprocessed entities without table references
        String message = String.format(
                "Batch write operation failed with %d batch(es) containing unprocessed items. " +
                "Unable to extract specific entities (requires DynamoDBTemplate migration to SDK v2).",
                failedBatches.size());

        if (targetType == BatchWriteException.class) {
            @SuppressWarnings("unchecked")
            T exception = (T) new BatchWriteException(
                    message,
                    List.of(), // Empty - can't extract without table references
                    0,
                    new RuntimeException(message));
            return exception;
        } else {
            throw new IllegalArgumentException(
                    "targetType must be BatchWriteException for SDK v2. Received: " + targetType.getName());
        }
    }
}