/*
* Copyright 2013-2014 the original author or authors.
*
* 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.springframework.cloud.aws.jdbc.retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.classify.BinaryExceptionClassifier;
import org.springframework.dao.TransientDataAccessException;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.context.RetryContextSupport;
import org.springframework.retry.policy.SimpleRetryPolicy;
import java.sql.SQLNonTransientConnectionException;
import java.sql.SQLRecoverableException;
import java.sql.SQLTransientException;
import java.util.HashMap;
import java.util.Map;
/**
* {@link RetryPolicy} implementation that checks for database error which are retryable. Normally this are well known
* exceptions inside the JDBC (1.6) exception hierarchy and also the Spring {@link
* org.springframework.dao.DataAccessException} hierarchy. In addition to that, this class also tries for permanent
* exception which are related to a connection of the database. This is useful because Amazon RDS database instances
* might be retryable even if there is a permanent error. This is typically the case in a master a/z failover where
* the source instance might not be available but a second attempt might succeed because the DNS record has been updated
* to the failover instance.
* <p>In contrast to a {@link SimpleRetryPolicy} this class also checks recursively the
* cause of the exception if there is a retryable implementation.</p>
*
* @author Agim Emruli
* @since 1.0
*/
public class SqlRetryPolicy implements RetryPolicy {
private static final Logger LOGGER = LoggerFactory.getLogger(SqlRetryPolicy.class);
/**
* BinaryExceptionClassifier used to classify exceptions
*/
private final BinaryExceptionClassifier binaryExceptionClassifier = new BinaryExceptionClassifier(getSqlRetryAbleExceptions(), false);
/**
* Holds the maximum number of retries that should be tried if an exception is retryable
*/
private int maxNumberOfRetries = 3;
/**
* Returns if this method is retryable based on the {@link RetryContext}. If there is no Throwable registered, then
* this method returns <code>true</code> without checking any further conditions. If there is a Throwable registered,
* this class checks if the registered Throwable is a retryable Exception in the context of SQL exception. If not
* successful, this class also checks the cause if there is a nested retryable exception available.
* <p>Before checking exception this class checks that the current retry count (fetched through {@link
* org.springframework.retry.RetryContext#getRetryCount()} is smaller or equals to the {@link #maxNumberOfRetries}</p>
*
* @param context
* - the retry context holding information about the retryable operation (number of retries, throwable if any)
* @return <code>true</code> if there is no throwable registered, if there is a retryable exception and the number of maximum
* numbers of retries have not been reached.
*/
@Override
public boolean canRetry(RetryContext context) {
Throwable candidate = context.getLastThrowable();
if (candidate == null) {
return true;
}
return context.getRetryCount() <= this.maxNumberOfRetries && isRetryAbleException(candidate);
}
@Override
public RetryContext open(RetryContext parent) {
return new RetryContextSupport(parent);
}
@Override
public void close(RetryContext context) {
}
@Override
public void registerThrowable(RetryContext context, Throwable throwable) {
((RetryContextSupport) context).registerThrowable(throwable);
}
private boolean isRetryAbleException(Throwable throwable) {
boolean retryAble = this.binaryExceptionClassifier.classify(throwable);
if (!retryAble) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Retry on Exception: {} not possible trying cause", throwable.getClass().getName());
}
if (throwable.getCause() != null) {
return isRetryAbleException(throwable.getCause());
}
return false;
} else {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Retry possible due to exception class {}", throwable.getClass().getName());
}
return true;
}
}
/**
* Configures the maximum number of retries. This number should be a trade-off between having enough retries to
* survive a database outage due to failure and a responsive and not stalling application. The default value for the
* maximum number is 3.
* <p><b>Note:</b>Consider using a {@link BackOffPolicy} which ensures that there is
* enough time left between the retry attempts instead of increasing this value to a high number. The back-off policy
* ensures that there is a delay in between the retry operations.</p>
*
* @param maxNumberOfRetries
* - the maximum number of retries should be a positive number, otherwise all retries will fail.
*/
public void setMaxNumberOfRetries(int maxNumberOfRetries) {
this.maxNumberOfRetries = maxNumberOfRetries;
}
/**
* Returns all the exceptions for which a retry is useful
*
* @return - Map containing all retryable exceptions for the {@link BinaryExceptionClassifier}
*/
private static Map<Class<? extends Throwable>, Boolean> getSqlRetryAbleExceptions() {
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(SQLTransientException.class, true);
retryableExceptions.put(SQLRecoverableException.class, true);
retryableExceptions.put(TransientDataAccessException.class, true);
retryableExceptions.put(SQLNonTransientConnectionException.class, true);
return retryableExceptions;
}
}