net.jodah.failsafe.internal.actions.ActionRegistry.java Source code

Java tutorial

Introduction

Here is the source code for net.jodah.failsafe.internal.actions.ActionRegistry.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or any later version.
 *
 * <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package net.jodah.failsafe.internal.actions;

import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.jodah.failsafe.ExecutionContext;
import net.jodah.failsafe.FailsafeController;
import net.jodah.failsafe.function.ContextualCallable;
import net.jodah.failsafe.internal.FailsafeContinueException;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The action registry class is used to track failsafe actions registered with a controller.
 *
 * @param <R> the result type
 */
public class ActionRegistry<R> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ActionRegistry.class);

    private static final String NL_AND_INDENT = String.format("%n\t");

    private final FailsafeController<R> controller;

    private final Deque<Expectation> expectations = new LinkedList<>();

    private final Deque<Expectation> processing = new LinkedList();

    private AssertionError shutdownFailure = null;

    public ActionRegistry(FailsafeController<R> controller) {
        this.controller = controller;
    }

    /**
     * Adds the specified action list as the next expected execution.
     *
     * @param actionList the list of expected actions for the next expected execution
     */
    public void add(ActionList<R> actionList) {
        synchronized (controller) {
            final Expectation expectation = new Expectation(expectations.size() + 1);

            actionList.populate(expectation);
            expectations.add(expectation);
        }
    }

    /**
     * Adds the specified action list as the next <code>count</code> expected executions.
     *
     * @param actionList the list of expected actions for the next expected executions
     * @param count the number of expected executions to register the list of actions with
     * @throws IllegalArgumentException if <code>count</code> is negative
     */
    public void add(ActionList<R> actionList, int count) {
        Validate.isTrue(count >= 0, "count must be greater or equal than 0");
        synchronized (controller) {
            for (int i = 0; i < count; i++) {
                final Expectation expectation = new Expectation(expectations.size() + 1);

                actionList.populate(expectation);
                expectations.add(expectation);
            }
        }
    }

    /**
     * Shuts down this registry.
     *
     * @param failure the associated failure to throw back if the registry is used afterward
     */
    public void shutdown(AssertionError failure) {
        synchronized (controller) {
            this.shutdownFailure = failure;
        }
    }

    /**
     * Verifies if all expected actions that cannot be left incomplete have been completed.
     *
     * @throws AssertionError if there are recorded actions left to execute
     */
    public void verify() {
        synchronized (controller) {
            processing.forEach(Expectation::verify);
            if (!expectations.isEmpty()) {
                throw controller.setFailure(new AssertionError(String.format(
                        "expecting %d executions for `%s` but only %d occurred; the following were not executed: %n\t%s%n",
                        (expectations.size() + processing.size()), controller, processing.size(),
                        expectations.stream().map(Expectation::toString)
                                .collect(Collectors.joining(ActionRegistry.NL_AND_INDENT)))));
            }
        }
    }

    /**
     * Gets the next expectation to be executed.
     *
     * @return the next expectation
     * @throws AssertionError if there are not enough expected executions recorded
     */
    public Expectation next() {
        synchronized (controller) {
            if (expectations.isEmpty()) {
                throw controller.setFailure(
                        new AssertionError("not enough expected executions recorded for '" + controller + "'"));
            }
            final Expectation expectation = expectations.pop();

            processing.add(expectation);
            return expectation;
        }
    }

    /** Holds all expected actions for a given Failsafe execution. */
    public class Expectation {
        /** Unique identifier for this expectation. */
        private final int id;

        private final Deque<Action<R>> actions = new LinkedList<>();

        private final Deque<Action<R>> processed = new LinkedList();

        private Expectation(int id) { // prevents creation from outside the class
            this.id = id;
        }

        public FailsafeController<R> getController() {
            return controller;
        }

        /**
         * Gets a unique identifier for this expectation with the associated controller.
         *
         * @return a unique identifier for this expectation
         */
        public int getId() {
            return id;
        }

        /**
         * Verifies if all recorded actions that cannot be left incomplete have been completed.
         *
         * @throws AssertionError if there are recorded actions left to execute
         */
        public void verify() {
            synchronized (controller) {
                final List<Action<R>> left = actions.stream().filter(a -> !a.canBeLeftIncomplete())
                        .collect(Collectors.toList());

                if (!left.isEmpty()) {
                    throw controller.setFailure(new AssertionError(String.format(
                            "too many expected actions for execution '%s - %s'; the following action(s) were not attempted: %n\t%s%n",
                            controller, id, left.stream().map(Action::currentToString)
                                    .collect(Collectors.joining(ActionRegistry.NL_AND_INDENT)))));
                }
            }
        }

        @SuppressWarnings("squid:S1181" /* bubbling up VirtualMachineError first */)
        public R attempt(ExecutionContext context, ContextualCallable<R> callable) throws Exception {
            LOGGER.debug("FailsafeController({} - {}): failsafe is attempting", controller, id);
            while (true) {
                final ActionContext<R> actionContext = new ActionContext<>(controller, context, callable);
                final Action<R> action = peek();

                try {
                    final R r = action.execute(actionContext);

                    if (r == Action.NOTHING) {
                        LOGGER.debug("FailsafeController({} - {}): action {} completed", controller, id, action);
                        return null; // nothing is returned back as null and will eventually be ignored
                    }
                    LOGGER.debug("FailsafeController({} - {}): action {} returned: {}", controller, id, action, r);
                    return r;
                } catch (VirtualMachineError e) {
                    throw e;
                } catch (FailsafeContinueException e) { // do nothing and loop back to continue with the next
                    LOGGER.debug("FailsafeController({} - {}): action {} indicated to continue with next action",
                            controller, id, action);
                } catch (Exception | Error e) {
                    LOGGER.debug("FailsafeController({} - {}): action {} threw: {}", controller, id, action, e, e);
                    throw e;
                } finally {
                    pop();
                }
            }
        }

        @Override
        public String toString() {
            return "execution #" + id + ": \r\n\t\t"
                    + actions.stream().map(Action::toString).collect(Collectors.joining("\r\n\t\t"));
        }

        /**
         * Adds the specified action to this registry.
         *
         * @param action the action to be added
         */
        void add(Action<R> action) {
            if (LOGGER.isDebugEnabled()) {
                if (action instanceof RepeatingAction) {
                    LOGGER.debug("FailsafeController({} - {}): decorating last recorded action [{}] as: {}",
                            controller, id, ((RepeatingAction<R>) action).getAction(), action);
                } else {
                    LOGGER.debug("FailsafeController({} - {}): recording action: {}", controller, id, action);
                }
            }
            synchronized (controller) {
                actions.addLast(action);
            }
        }

        /**
         * Removes the last recorded action.
         *
         * @return the last recorded action
         * @throws java.util.NoSuchElementException if there are no actions recorded
         */
        Action<R> removeLast() {
            synchronized (controller) {
                return actions.removeLast();
            }
        }

        /**
         * Peeks at the next action to be executed.
         *
         * @return the next action to be executed
         * @throws AssertionError if there are no more actions in the registry (the controller's will be
         *     updated with a corresponding failure)
         */
        private Action<R> peek() {
            synchronized (controller) {
                failIfShutdown();
                final Action<R> action = actions.peek();

                if (action == null) {
                    throw controller.setFailure(new AssertionError(String.format(
                            "not enough expected actions for execution '%s - %s'; the following %d action(s) were processed: %n\t%s%n",
                            controller, id, processed.size(), processed.stream().map(Action::definedToString)
                                    .collect(Collectors.joining(ActionRegistry.NL_AND_INDENT)))));
                }
                LOGGER.debug("FailsafeController({} - {}): next action to execute: {}", controller, id, action);
                return action;
            }
        }

        /**
         * Pops the next action to be executed if it has completed.
         *
         * @return the next action if it has completed or <code>null</code> if it hasn't or if no
         *     actions are left
         */
        @Nullable
        private Action<R> pop() {
            synchronized (controller) {
                final Action<R> action = actions.peek();

                if ((action != null) && action.hasCompleted()) {
                    LOGGER.debug("FailsafeController({} - {}): completed action: {}", controller, id, action);
                    actions.pop();
                    processed.add(action);
                }
                return action;
            }
        }

        /**
         * Checks if the registry was shutdown and throw back an assertion error if it has.
         *
         * @throws AssertionError if the registry was shutdown
         */
        private void failIfShutdown() {
            synchronized (controller) {
                if (shutdownFailure != null) {
                    throw shutdownFailure;
                }
            }
        }
    }
}