All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elder.sourcerer.crud.CommandUtils Maven / Gradle / Ivy

package org.elder.sourcerer.crud;

import org.elder.sourcerer.CommandResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * Utility methods for constructing a synchronous facade from independent but related methods. Used
 * to (re)create a traditional synchronous create/update operation on top of the command/query
 * pattern by first executing the command, then waiting until the entity appears on the read side.
 */
public final class CommandUtils {
    private static final Logger logger = LoggerFactory.getLogger(CommandUtils.class);

    private CommandUtils() {
    }

    /**
     * Waits for the presence of an entity in a read model, polling until the model asserts the
     * presence of the entity at the given version.
     *
     * @param assertFunction A that should complete without exceptions if the relevant read model
     *                       now has the entity with the given id and version. On any exception (but
     *                       not other throwables), this method will be retried up to the maximum
     *                       number of attempts specified, or until successful.
     * @throws ReadAssertFailedException if the maximum number of retries have been attempted and
     *                                   the assertion still fails.
     */
    public static void waitForUpdate(final Runnable assertFunction) {
        waitForUpdate(
                assertFunction,
                Duration.ofMillis(50),
                Duration.ofMillis(50),
                1.5,
                12);
    }

    /**
     * Waits for the presence of an entity in a read model, polling until the model asserts the
     * presence of the entity at the given version.
     *
     * @param assertFunction   A that should complete without exceptions if the relevant read model
     *                         now has the entity with the given id and version. On any exception
     *                         (but not other throwables), this method will be retried up to the
     *                         maximum number of attempts specified, or until successful.
     * @param initialDelay     The amount of time to wait between the command succeeding, and the
     *                         first attempt to call the assert function.
     * @param retryDelay       The amount of time to wait after the first failed attempt to assert
     *                         the presence of the aggregate, before retrying again.
     * @param retryDelayFactor For exponential backoff - the factor to use to increase the delay
     *                         between successive attempts. I.e. for a factor 2 and retry delay
     *                         100ms, the delays between attempts will be 100ms, 200ms, 400ms etc.
     * @param maxRetryAttempts The maximum number of attempts before giving up on the operation.
     * @throws ReadAssertFailedException if the maximum number of retries have been attempted and
     *                                   the assertion still fails.
     */
    public static void waitForUpdate(
            final Runnable assertFunction,
            final Duration initialDelay,
            final Duration retryDelay,
            final double retryDelayFactor,
            final int maxRetryAttempts) {
        sleepSoundly(initialDelay.toMillis());
        int attemptsLeft = maxRetryAttempts;
        long currentSleepMillis = initialDelay.toMillis();
        long currentRetryDelay = retryDelay.toMillis();

        Exception lastAssertError = null;
        do {
            sleepSoundly(currentSleepMillis);
            try {
                assertFunction.run();
                logger.debug("Assert successful");
                return;
            } catch (Exception ex) {
                logger.debug("Assert failed: {}", ex.getMessage());
                lastAssertError = ex;
                currentSleepMillis = (long) (currentRetryDelay * retryDelayFactor);
                currentRetryDelay = currentSleepMillis;
                attemptsLeft--;
            }
        }
        while (attemptsLeft > 0);

        logger.warn(
                "Retries exhausted after {} attempts, last error:",
                maxRetryAttempts,
                lastAssertError);
        throw new ReadAssertFailedException(lastAssertError);
    }

    /**
     * Executes a command, and waits for the affected entity to be updated in the relevant read
     * model. This method will execute the command once, propagating any exceptions immediately. If
     * the command is successful, it will use the provided assert function to check that it has been
     * created, retrying periodically as required until completed.
     *
     * @param commandFunction A function that, when called, executes the command and returns the id
     *                        of the affected entity along with its new version.
     * @param assertFunction  A function accepting an aggregate id and version that should complete
     *                        without exceptions if the relevant read model now has the entity with
     *                        the given id and version. On any exception (but not other throwables),
     *                        this method will be retried up to the maximum number of attempts
     *                        specified, or until successful.
     * @return The command response as returned from the command function.
     * @throws ReadAssertFailedException if the maximum number of retries have been attempted and
     *                                   the assertion still fails.
     */
    public static CommandResponse executeSynchronously(
            final Supplier commandFunction,
            final BiConsumer assertFunction) {
        return executeSynchronously(
                commandFunction,
                assertFunction,
                Duration.ofMillis(50),
                Duration.ofMillis(50),
                1.5,
                12);
    }

    /**
     * Executes a command, and waits for the affected entity to be updated in the relevant read
     * model. This method will execute the command once, propagating any exceptions immediately. If
     * the command is successful, it will use the provided assert function to check that it has been
     * created, retrying periodically as required until completed.
     *
     * @param commandFunction  A function that, when called, executes the command and returns the id
     *                         of the affected entity along with its new version.
     * @param assertFunction   A function accepting an aggregate id and version that should complete
     *                         without exceptions if the relevant read model now has the entity with
     *                         the given id and version. On any exception (but not other
     *                         throwables), this method will be retried up to the maximum number of
     *                         attempts specified, or until successful.
     * @param initialDelay     The amount of time to wait between the command succeeding, and the
     *                         first attempt to call the assert function.
     * @param retryDelay       The amount of time to wait after the first failed attempt to assert
     *                         the presence of the aggregate, before retrying again.
     * @param retryDelayFactor For exponential backoff - the factor to use to increase the delay
     *                         between successive attempts. I.e. for a factor 2 and retry delay
     *                         100ms, the delays between attempts will be 100ms, 200ms, 400ms etc.
     * @param maxRetryAttempts The maximum number of attempts before giving up on the operation.
     * @return The command response as returned from the command function.
     * @throws ReadAssertFailedException if the maximum number of retries have been attempted and
     *                                   the assertion still fails.
     */
    public static CommandResponse executeSynchronously(
            final Supplier commandFunction,
            final BiConsumer assertFunction,
            final Duration initialDelay,
            final Duration retryDelay,
            final double retryDelayFactor,
            final int maxRetryAttempts) {
        CommandResponse commandResponse = commandFunction.get();
        if (commandResponse.getNoOp() != null && commandResponse.getNoOp()) {
            logger.debug("Command was no-op, not waiting around");
            return commandResponse;
        }

        Integer expectedVersion = commandResponse.getNewVersion();
        if (expectedVersion == null) {
            throw new IllegalArgumentException("Command was not no-op but returned no version!");
        }

        waitForUpdate(
                () -> assertFunction.accept(commandResponse.getId(), expectedVersion),
                initialDelay,
                retryDelay,
                retryDelayFactor,
                maxRetryAttempts);

        return commandResponse;
    }

    private static void sleepSoundly(final long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ex) {
            logger.warn("Interrupted while sleeping, ignoring");
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy