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

io.bitsensor.plugins.shaded.org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2006-2016 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 io.bitsensor.plugins.shaded.org.springframework.retry.interceptor;

import java.util.Arrays;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import io.bitsensor.plugins.shaded.org.apache.commons.logging.Log;
import io.bitsensor.plugins.shaded.org.apache.commons.logging.LogFactory;

import io.bitsensor.plugins.shaded.org.springframework.classify.Classifier;
import io.bitsensor.plugins.shaded.org.springframework.retry.RecoveryCallback;
import io.bitsensor.plugins.shaded.org.springframework.retry.RetryCallback;
import io.bitsensor.plugins.shaded.org.springframework.retry.RetryContext;
import io.bitsensor.plugins.shaded.org.springframework.retry.RetryOperations;
import io.bitsensor.plugins.shaded.org.springframework.retry.RetryState;
import io.bitsensor.plugins.shaded.org.springframework.retry.policy.NeverRetryPolicy;
import io.bitsensor.plugins.shaded.org.springframework.retry.support.DefaultRetryState;
import io.bitsensor.plugins.shaded.org.springframework.retry.support.RetryTemplate;
import io.bitsensor.plugins.shaded.org.springframework.util.Assert;
import io.bitsensor.plugins.shaded.org.springframework.util.ObjectUtils;
import io.bitsensor.plugins.shaded.org.springframework.util.StringUtils;

/**
 * A {@link MethodInterceptor} that can be used to automatically retry calls to a method
 * on a service if it fails. The argument to the service method is treated as an item to
 * be remembered in case the call fails. So the retry operation is stateful, and the item
 * that failed is tracked by its unique key (via {@link MethodArgumentsKeyGenerator})
 * until the retry is exhausted, at which point the {@link MethodInvocationRecoverer} is
 * called.
 *
 * The main use case for this is where the service is transactional, via a transaction
 * interceptor on the interceptor chain. In this case the retry (and recovery on
 * exhausted) always happens in a new transaction.
 *
 * The injected {@link RetryOperations} is used to control the number of retries. By
 * default it will retry a fixed number of times, according to the defaults in
 * {@link RetryTemplate}.
 *
 * @author Dave Syer
 * @author Gary Russell
 */
public class StatefulRetryOperationsInterceptor implements MethodInterceptor {

	private transient final Log logger = LogFactory.getLog(getClass());

	private MethodArgumentsKeyGenerator keyGenerator;

	private MethodInvocationRecoverer recoverer;

	private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;

	private RetryOperations retryOperations;

	private String label;

	private Classifier rollbackClassifier;

	private boolean useRawKey;

	public StatefulRetryOperationsInterceptor() {
		RetryTemplate retryTemplate = new RetryTemplate();
		retryTemplate.setRetryPolicy(new NeverRetryPolicy());
		this.retryOperations = retryTemplate;
	}

	public void setRetryOperations(RetryOperations retryTemplate) {
		Assert.notNull(retryTemplate, "'retryOperations' cannot be null.");
		this.retryOperations = retryTemplate;
	}

	/**
	 * Public setter for the {@link MethodInvocationRecoverer} to use if the retry is
	 * exhausted. The recoverer should be able to return an object of the same type as the
	 * target object because its return value will be used to return to the caller in the
	 * case of a recovery.
	 * @param recoverer the {@link MethodInvocationRecoverer} to set
	 */
	public void setRecoverer(MethodInvocationRecoverer recoverer) {
		this.recoverer = recoverer;
	}

	/**
	 * Rollback classifier for the retry state. Default to null (meaning rollback for
	 * all).
	 *
	 * @param rollbackClassifier the rollbackClassifier to set
	 */
	public void setRollbackClassifier(
			Classifier rollbackClassifier) {
		this.rollbackClassifier = rollbackClassifier;
	}

	public void setKeyGenerator(MethodArgumentsKeyGenerator keyGenerator) {
		this.keyGenerator = keyGenerator;
	}

	public void setLabel(String label) {
		this.label = label;
	}

	/**
	 * Public setter for the {@link NewMethodArgumentsIdentifier}. Only set this if the
	 * arguments to the intercepted method can be inspected to find out if they have never
	 * been processed before.
	 * @param newMethodArgumentsIdentifier the {@link NewMethodArgumentsIdentifier} to set
	 */
	public void setNewItemIdentifier(
			NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
		this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier;
	}

	/**
	 * Set to true to use the raw key generated by the key generator. Should only be set
	 * to true for cases where the key is guaranteed to be unique in all cases. When
	 * false, a compound key is used, including invocation metadata.
	 * Default: false.
	 * @param useRawKey the useRawKey to set.
	 */
	public void setUseRawKey(boolean useRawKey) {
		this.useRawKey = useRawKey;
	}

	/**
	 * Wrap the method invocation in a stateful retry with the policy and other helpers
	 * provided. If there is a failure the exception will generally be re-thrown. The only
	 * time it is not re-thrown is when retry is exhausted and the recovery path is taken
	 * (though the {@link MethodInvocationRecoverer} provided if there is one). In that
	 * case the value returned from the method invocation will be the value returned by
	 * the recoverer (so the return type for that should be the same as the intercepted
	 * method).
	 * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
	 * @see MethodInvocationRecoverer#recover(Object[], Throwable)
	 *
	 */
	@Override
	public Object invoke(final MethodInvocation invocation) throws Throwable {

		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Executing proxied method in stateful retry: "
					+ invocation.getStaticPart() + "("
					+ ObjectUtils.getIdentityHexString(invocation) + ")");
		}

		Object[] args = invocation.getArguments();
		Object defaultKey = Arrays.asList(args);
		if (args.length == 1) {
			defaultKey = args[0];
		}

		Object key = createKey(invocation, defaultKey);
		RetryState retryState = new DefaultRetryState(key,
				this.newMethodArgumentsIdentifier != null
						&& this.newMethodArgumentsIdentifier.isNew(args),
				this.rollbackClassifier);

		Object result = this.retryOperations
				.execute(new MethodInvocationRetryCallback(invocation, label),
						this.recoverer != null
								? new ItemRecovererCallback(args, this.recoverer) : null,
						retryState);

		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Exiting proxied method in stateful retry with result: ("
					+ result + ")");
		}

		return result;

	}

	private Object createKey(final MethodInvocation invocation, Object defaultKey) {
		Object generatedKey = defaultKey;
		if (this.keyGenerator != null) {
			generatedKey = this.keyGenerator.getKey(invocation.getArguments());
		}
		if (generatedKey == null) {
			// If there's a generator and he still says the key is null, that means he
			// really doesn't want to retry.
			return null;
		}
		if (this.useRawKey) {
			return generatedKey;
		}
		String name = StringUtils.hasText(label) ? label
				: invocation.getMethod().toGenericString();
		return Arrays.asList(name, generatedKey);
	}

	/**
	 * @author Dave Syer
	 *
	 */
	private static final class MethodInvocationRetryCallback
			implements RetryCallback {

		private final MethodInvocation invocation;
		private String label;

		private MethodInvocationRetryCallback(MethodInvocation invocation, String label) {
			this.invocation = invocation;
			if (StringUtils.hasText(label)) {
				this.label = label;
			}
			else {
				this.label = invocation.getMethod().toGenericString();
			}
		}

		@Override
		public Object doWithRetry(RetryContext context) throws Exception {
			context.setAttribute(RetryContext.NAME, label);
			try {
				return this.invocation.proceed();
			}
			catch (Exception e) {
				throw e;
			}
			catch (Error e) {
				throw e;
			}
			catch (Throwable e) {
				throw new IllegalStateException(e);
			}
		}
	}

	/**
	 * @author Dave Syer
	 *
	 */
	private static final class ItemRecovererCallback implements RecoveryCallback {

		private final Object[] args;

		private final MethodInvocationRecoverer recoverer;

		/**
		 * @param args the item that failed.
		 */
		private ItemRecovererCallback(Object[] args,
				MethodInvocationRecoverer recoverer) {
			this.args = Arrays.asList(args).toArray();
			this.recoverer = recoverer;
		}

		@Override
		public Object recover(RetryContext context) {
			return this.recoverer.recover(this.args, context.getLastThrowable());
		}

	}

}