/***************************************************************************** * Copyright (C) google.com * * ------------------------------------------------------------------------- * * 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 com.google.mu.util; import static com.google.mu.util.Utils.typed; import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import com.google.mu.util.Retryer.Delay; /** * Configures an abstract plan based on exceptions. * * This class is immutable. * * Each exceptional rule is configured with a list of abstract strategies. * Users are expected to use the continuation returned by {@link #execute} upon any exception. * * The returned {@link Execution} includes both the current strategy for the exception in * question, and a new {@code ExeceptionPlan} object for upcoming exceptions in the same logical * group (such as exceptions thrown by the same method invocation in a retry). * * <p>For {@link Retryer}, {@code T} can be {@link Delay} between retries. But any strategy types * work too. * * <p>Strategies specified through {@link #upon} are picked with * respect to the order they are added. Think of them as a bunch of * {@code if ... else if ...}. That is, <em>always specify the more specific exception first</em>. * The following code is wrong because the {@code IOException} strategies will always be * overshadowed by the {@code Exception} strategies. * <pre>{@code * ExceptionPlan<Duration> = new ExceptionPlan<Duration>() * .upon(Exception.class, exponentialBackoff(...)) * .upon(IOException.class, uniformDelay(...)); * }</pre> */ final class ExceptionPlan<T> { private final List<Rule<T>> rules; public ExceptionPlan() { this.rules = Collections.emptyList(); } private ExceptionPlan(List<Rule<T>> rules) { this.rules = rules; } /** * Returns a new {@link ExceptionPlan} that uses {@code strategies} when an exception satisfies * {@code condition}. */ public ExceptionPlan<T> upon( Predicate<? super Throwable> condition, List<? extends T> strategies) { List<Rule<T>> newRules = new ArrayList<>(rules); newRules.add(new Rule<T>(condition, strategies)); return new ExceptionPlan<>(newRules); } /** * Returns a new {@link ExceptionPlan} that uses {@code strategies} when an exception is * instance of {@code exceptionType}. */ public ExceptionPlan<T> upon( Class<? extends Throwable> exceptionType, List<? extends T> strategies) { return upon(exceptionType::isInstance, strategies); } /** * Returns a new {@link ExceptionPlan} that uses {@code strategies} when an exception is instance * of {@code exceptionType} and satisfies {@code condition}. */ public <E extends Throwable> ExceptionPlan<T> upon( Class<E> exceptionType, Predicate<? super E> condition, List<? extends T> strategies) { return upon(typed(exceptionType, condition) , strategies); } /** * Executes the plan and either returns an {@link Execution} or throws {@code E} if the exception * isn't covered by the plan or the plan decides to propagate. */ public <E extends Throwable> Maybe<Execution<T>, E> execute(E exception) { requireNonNull(exception); List<Rule<T>> remainingRules = new ArrayList<>(); Rule<T> applicable = null; for (Rule<T> rule : rules) { if (applicable == null && rule.appliesTo(exception)) { applicable = rule; remainingRules.add(rule.remaining()); } else { remainingRules.add(rule); } } if (applicable == null) return Maybe.except(exception); return applicable.currentStrategy() .map(s -> new Execution<>(s, new ExceptionPlan<>(remainingRules))) .map(Maybe::<Execution<T>, E>of) .orElse(Maybe.except(exception)); // The rule refuses to handle it. } /** Returns {@code true} if {@code exception} is covered in this plan. */ public boolean covers(Throwable exception) { requireNonNull(exception); return rules.stream().anyMatch(rule -> rule.appliesTo(exception)); } /** Describes what to do for the given exception. */ public static final class Execution<T> { private final T strategy; private final ExceptionPlan<T> exceptionPlan; Execution(T stragety, ExceptionPlan<T> exceptionPlan) { this.strategy = requireNonNull(stragety); this.exceptionPlan = requireNonNull(exceptionPlan); } /** Returns the strategy to handle the current exception. Up to caller to interpret. */ public T strategy() { return strategy; } /** Returns the exception plan for remaining exceptions. */ public ExceptionPlan<T> remainingExceptionPlan() { return exceptionPlan; } } private static final class Rule<T> { private final Predicate<? super Throwable> condition; private final List<? extends T> strategies; private final int index; private Rule(Predicate<? super Throwable> condition, List<? extends T> strategies, int index) { this.condition = requireNonNull(condition); this.strategies = requireNonNull(strategies); this.index = index; } Rule(Predicate<? super Throwable> condition, List<? extends T> strategies) { this(condition, strategies, 0); } boolean appliesTo(Throwable exception) { return condition.test(exception); } Rule<T> remaining() { return new Rule<>(condition, strategies, index + 1); } Optional<T> currentStrategy() { if (index >= strategies.size()) return Optional.empty(); try { return Optional.of(strategies.get(index)); } catch (IndexOutOfBoundsException e) { // In case the list just changed due to race condition or side-effects. return Optional.empty(); } } } }