/*
* ToroDB
* Copyright © 2014 8Kdata Technology (www.8kdata.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.torodb.core.retrier;
import com.torodb.common.util.RetryHelper;
import com.torodb.common.util.RetryHelper.DelegateExceptionHandler;
import com.torodb.common.util.RetryHelper.ExceptionHandler;
import com.torodb.common.util.RetryHelper.IncrementalWaitExceptionHandler;
import com.torodb.common.util.RetryHelper.RetryCallback;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.EnumSet;
import java.util.function.IntBinaryOperator;
import java.util.function.IntPredicate;
/**
*
*/
public class SmartRetrier extends AbstractHintableRetrier {
private static final Logger LOGGER = LogManager.getLogger(SmartRetrier.class);
private final IntPredicate criticalGiveUpPredicate;
private final IntPredicate infrequentGiveUpPredicate;
private final IntPredicate frequentGiveUpPredicate;
private final IntPredicate defaultGiveUpPredicate;
private final MillisToWaitFunction millisToWaitFunction;
public SmartRetrier(IntPredicate criticalGiveUpPredicate, IntPredicate infrequentGiveUpPredicate,
IntPredicate frequentGiveUpPredicate, IntPredicate defaultGiveUpPredicate,
MillisToWaitFunction millisToWaitFunction) {
this.infrequentGiveUpPredicate = infrequentGiveUpPredicate;
this.frequentGiveUpPredicate = frequentGiveUpPredicate;
this.defaultGiveUpPredicate = defaultGiveUpPredicate;
this.criticalGiveUpPredicate = criticalGiveUpPredicate;
this.millisToWaitFunction = millisToWaitFunction;
}
@Override
protected <R, T extends Exception> ExceptionHandler<R, T> getExceptionHandler(
EnumSet<Hint> hints, ExceptionHandler<R, T> delegateHandler) {
IntPredicate giveUpPredicate = getGiveUpPredicate(hints);
if (hints.contains(Hint.TIME_SENSIBLE)) {
return createWithTimeHandler(giveUpPredicate, delegateHandler);
} else {
return createWithoutTimeHandler(giveUpPredicate, delegateHandler);
}
}
private IntPredicate getGiveUpPredicate(EnumSet<Hint> hints) {
if (hints.contains(Hint.CRITICAL)) {
return criticalGiveUpPredicate;
}
if (hints.contains(Hint.INFREQUENT_ROLLBACK)) {
return infrequentGiveUpPredicate;
}
if (hints.contains(Hint.FREQUENT_ROLLBACK)) {
return frequentGiveUpPredicate;
}
return defaultGiveUpPredicate;
}
/**
*
* @param millis
* @param attempts
* @param giveUpPredicate
* @return A value that follows the semantic of
* {@link RetryHelper#IncrementalWaitExceptionHandler} int binary operator, meaning that a
* zero or positive value indicates the number of millis to wait and a negative one
* indicates that the handler should give up
*/
private int getMillisToWait(int attempts, int millis, IntPredicate giveUpPredicate) {
if (giveUpPredicate.test(attempts)) {
LOGGER.debug(
"Giving up when executing a task after {} executions (last execution took {} millis)",
attempts, millis);
return -1;
}
int millisToWait = millisToWaitFunction.applyAsInt(attempts, millis);
if (LOGGER.isTraceEnabled()) {
int newAttempt = attempts + 1;
if (millisToWait == 0) {
LOGGER.trace("Trying to execute a task for {}th time (last "
+ "execution took {} millis)", newAttempt, millis);
} else if (millisToWait > 0) {
LOGGER.trace("Sleeping {} millis before trying to execute a "
+ "task for {}th time", millisToWait, newAttempt);
} else {
assert millisToWait < 0;
LOGGER.debug("Giving up when executing a task after {} "
+ "executions (last execution took {} millis)", attempts, millis);
}
}
return millisToWait;
}
private <R, T extends Exception> ExceptionHandler<R, T> createWithoutTimeHandler(
IntPredicate giveUpPredicate, ExceptionHandler<R, T> delegateHandler) {
ExceptionHandler<R, T> delegate = (RetryCallback<R> callback,
Exception t, int attempts) -> {
if (giveUpPredicate.test(attempts)) {
LOGGER.debug("Giving up when executing a task after {} "
+ "executions", attempts, t);
delegateHandler.handleException(callback, t, attempts);
} else {
LOGGER.trace("Trying to execute a task for {}th time",
attempts);
callback.doRetry();
}
};
return loggerHandler(delegate);
}
private <R, T extends Exception> ExceptionHandler<R, T> createWithTimeHandler(
IntPredicate giveUpPredicate, ExceptionHandler<R, T> delegateHandler) {
ExceptionHandler<R, T> delegate = new IncrementalWaitExceptionHandler<>(
(millis, attempts) -> getMillisToWait(attempts, millis, giveUpPredicate),
delegateHandler
);
return loggerHandler(delegate);
}
private <R, T extends Exception> ExceptionHandler<R, T> loggerHandler(
ExceptionHandler<R, T> delegate) {
ExceptionHandler<R, T> result;
if (LOGGER.isDebugEnabled()) {
result = new DelegateExceptionHandler<R, T>(delegate) {
@Override
public void handleException(RetryCallback<R> callback,
Exception t, int attempts) throws T {
LOGGER.debug("Exception catched on the replier", t);
super.handleException(callback, t, attempts);
}
};
} else {
result = delegate;
}
return result;
}
@FunctionalInterface
public static interface MillisToWaitFunction extends IntBinaryOperator {
/**
*
* @param attempts the number of times this task was executed and failed
* @param millis the number of milliseconds that had been waited on the previous iteration
* @return the number of milliseconds that should be waited before the next iteration. If the
* number is negative, the executor gives up
*/
@Override
public int applyAsInt(int attempts, int millis);
}
}