Official account: Java Xiaokaxiu, website: Javaxks.com

Author: Alben ‘s home, link: albenw. Making. IO/posts / 69 a96…

Spring implements a retry mechanism that is simple and practical. This article explains how to use Spring Retry and how it works.

Implementation principles of Spring-retry Retry

Published in the 2019-03-01 | classification in Java, spring | 2 | read number: 2252

Word count: 3.7 k words | reading material 17 minutes long

Spring implements a retry mechanism that is simple and practical. This article explains how to use Spring Retry and how it works.

The profile

Spring implements a retry mechanism that is simple and practical. Spring Retry is an independent feature of Spring Batch and is widely used in Spring Batch,Spring Integration, Spring for Apache Hadoop and other Spring projects. This article explains how to use Spring Retry and how it works.

background

Retry, in fact, we actually need a lot of times, in order to ensure fault tolerance, availability, consistency, etc. It is used to deal with unexpected returns and exceptions of external systems, especially network delay and interruption. In addition, popular microservices governance frameworks usually have their own retry and timeout configurations. For example, dubbo can set retries=1 and timeout=500 to retry only once after a call fails. If the call fails after 500ms, the call fails. So if we’re going to retry, if we’re going to retry for a particular operation, we’re going to hardcode it, and basically the logic is to write a loop, count the number of failures, based on returns or exceptions, and then set an exit condition. This, not to mention writing similar code for every operation, and mixing retry logic with business logic, creates maintenance and extension headaches. From an object-oriented point of view, we should keep the retry code separate.

Used to introduce

The basic use

Here’s an example:

@Configuration
@EnableRetry
public class Application {

    @Bean
    public RetryService retryService(){
        return new RetryService();
    }

    public static void main(String[] args) throws Exception{
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext("springretry");
        RetryService service1 = applicationContext.getBean("service", RetryService.class);
        service1.service();
    }
}

@Service("service")
public class RetryService {

    @Retryable(value = IllegalAccessException.class, maxAttempts = 5,
            backoff= @Backoff(value = 1500, maxDelay = 100000, multiplier = 1.2))
    public void service() throws IllegalAccessException {
        System.out.println("service method...");
        throw new IllegalAccessException("manual exception");
    }

    @Recover
    public void recover(IllegalAccessException e){
        System.out.println("service retry after Recover => " + e.getMessage());
    }

}
Copy the code

@retryable – Indicates that the retry mechanism is enabled. @retryable – Indicates that the method needs to be retried and has rich parameters that can meet your retry requirements. @backoff – Indicates the retried retreat policy. This method is executed after multiple retries and still fails

Spring-retry is rich in its Retry and fallback policies, as well as backstop, listener, and other operations.

And then the parameters in each of these annotations are very simple, so you can see what they mean and how they work, but I won’t go into them.

Retry strategy

Take a look at some of the Retry strategies that Spring Retry comes with, primarily to determine if a method call needs to be retried if it fails. (The implementation will be further analyzed in the Principles section below)

[] (Albenw. Making. IO/images/Spri…Implementation Principles of Retry Retry __0.png)

  • SimpleRetryPolicy A maximum of three retries are allowed by default
  • TimeoutRetryPolicy Defaults to retry all failures within 1 second
  • ExpressionRetryPolicy will be retried if it matches the expression
  • CircuitBreakerRetryPolicy increased by fusing mechanism, if not fuse, is allowed to try again
  • CompositeRetryPolicy Can compose multiple retry policies
  • NeverRetryPolicy never retries
  • AlwaysRetryPolicy always retry

… ., etc.

Retreat strategy

Take a look at the fallback strategy. Fallback is how to do the next retry, in this case how long to wait. (The implementation will be further analyzed in the Principles section below)

[] (Albenw. Making. IO/images/Spri…Implementation Principles of Retry Retry __1.png)

  • FixedBackOffPolicy Default fixed delay of 1 second before the next retry
  • ExponentialBackOffPolicy increases the retry delay exponentially. The default value is 0.1 seconds, and the coefficient is 2. Then the next retry delay is 0.2 seconds, and the next retry delay is 0.4 seconds, and so on, and the maximum delay is 30 seconds.
  • Add the randomness ExponentialRandomBackOffPolicy above that strategy
  • UniformRandomBackOffPolicy this keep up with the difference is that the delay will keep increasing, this will only be in a fixed interval random
  • StatelessBackOffPolicy This statement is stateless, and stateless is not aware of the last retreat, as can be seen from its subclasses

The principle of

The principle part I want to separate into two parts, one is the entry point of retry mechanism, that is, how it enables your code to retry; The second is the details of retry mechanism, including the logic of retry and the implementation of retry strategy and retreat strategy.

The breakthrough point

@EnableRetry

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @EnableAspectJAutoProxy(proxyTargetClass = false) @Import(RetryConfiguration.class) @Documented public @interface EnableRetry { /** * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed * to standard Java interface-based proxies. The default is {@code false}. *  * @return whether to proxy or not to proxy the class */ boolean proxyTargetClass() default false; }Copy the code

You can see @enableAspectJAutoProxy (proxyTargetClass = false). @import (retryConfiguration.class) @import is equivalent to registering this Bean

Let’s see what the RetryConfiguration is

[] (Albenw. Making. IO/images/Spri…Implementation Principles of Retry Retry __2.png)

It is an AbstractPointcutAdvisor that has a pointcut and an advice. As we know, during the IOC process, beans are Pointcut filtered according to the PointcutAdvisor class, and then corresponding AOP proxy classes are generated to enhance processing with advice. Take a look at the RetryConfiguration initialization:

@PostConstruct public void init() { Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1); retryableAnnotationTypes.add(Retryable.class); // create pointcut this. pointCut = buildPointcut(retryableAnnotationTypes); // create advice this.advice = buildAdvice(); if (this.advice instanceof BeanFactoryAware) { ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); } } protected Pointcut buildPointcut(Set<Class<? extends Annotation>> retryAnnotationTypes) { ComposablePointcut result = null; for (Class<? extends Annotation> retryAnnotationType : retryAnnotationTypes) { Pointcut filter = new AnnotationClassOrMethodPointcut(retryAnnotationType); if (result == null) { result = new ComposablePointcut(filter); } else { result.union(filter); } } return result; }Copy the code

Code above USES AnnotationClassOrMethodPointcut, in fact, it is finally used to AnnotationMethodMatcher for breakthrough point of the filter according to the annotation. This is the @retryable annotation.

// Create advice object, The interceptor protected Advice buildAdvice () {/ / pay attention to the object under the AnnotationAwareRetryOperationsInterceptor interceptor = new AnnotationAwareRetryOperationsInterceptor(); if (retryContextCache ! = null) { interceptor.setRetryContextCache(retryContextCache); } if (retryListeners ! = null) { interceptor.setListeners(retryListeners); } if (methodArgumentsKeyGenerator ! = null) { interceptor.setKeyGenerator(methodArgumentsKeyGenerator); } if (newMethodArgumentsIdentifier ! = null) { interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier); } if (sleeper ! = null) { interceptor.setSleeper(sleeper); } return interceptor; }Copy the code

AnnotationAwareRetryOperationsInterceptor

Inheritance relationships

[] (Albenw. Making. IO/images/Spri…Retry Retry Implementation Principles __3.png)

Can see AnnotationAwareRetryOperationsInterceptor is a MethodInterceptor, in the process of creating AOP agent if the target method conforms to the rules of the pointcut, It will be added to the Interceptor list and then enhanced. Let’s see what enhancements the Invoke method does.

@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
		if (delegate != null) {
			return delegate.invoke(invocation);
		}
		else {
			return invocation.proceed();
		}
	}
Copy the code

Delegate is used here. It depends on the configuration to delegate to a specific “stateful” or “stateless” interceptor.

private MethodInterceptor getDelegate(Object target, Method method) { if (! this.delegates.containsKey(target) || ! this.delegates.get(target).containsKey(method)) { synchronized (this.delegates) { if (! this.delegates.containsKey(target)) { this.delegates.put(target, new HashMap<Method, MethodInterceptor>()); } Map<Method, MethodInterceptor> delegatesForTarget = this.delegates.get(target); if (! delegatesForTarget.containsKey(method)) { Retryable retryable = AnnotationUtils.findAnnotation(method, Retryable.class); if (retryable == null) { retryable = AnnotationUtils.findAnnotation(method.getDeclaringClass(), Retryable.class); } if (retryable == null) { retryable = findAnnotationOnTarget(target, method); } if (retryable == null) { return delegatesForTarget.put(method, null); } MethodInterceptor delegate; // Supports custom MethodInterceptor, If (stringutils.hastext (retryable.interceptor())) {delegate = this.beanfactory.getbean (retryable.interceptor(),  MethodInterceptor.class); } else if (retryable. Stateful ()) {// Get a stateful interceptor delegate = getStatefulInterceptor(target, method, retryable); } else {// Get the "stateless" interceptor delegate = getStatelessInterceptor(target, method, retryable); } delegatesForTarget.put(method, delegate); } } } return this.delegates.get(target).get(method); }Copy the code

GetStatefulInterceptor and getStatelessInterceptor are pretty much the same. Let’s look at the simpler getStatelessInterceptor.

private MethodInterceptor getStatelessInterceptor(Object target, Method method, Retryable Retryable) {// Generate a RetryTemplate RetryTemplate template = createTemplate(Retryable.listeners()); // Generate retryPolicy template.setretryPolicy (getRetryPolicy(retryable)); / / generated backoffPolicy template. SetBackOffPolicy (getBackoffPolicy (retryable. Backoff ())); return RetryInterceptorBuilder.stateless() .retryOperations(template) .label(retryable.label()) .recoverer(getRecoverer(target, method)) .build(); }Copy the code

We’ll come back to the rules that generate retryPolicy and backoffPolicy. RetryInterceptorBuilder is to generate RetryOperationsInterceptor. RetryOperationsInterceptor is also a MethodInterceptor, let’s take a look at it’s invoke method.

public Object invoke(final MethodInvocation invocation) throws Throwable { String name; if (StringUtils.hasText(label)) { name = label; } else { name = invocation.getMethod().toGenericString(); } final String label = name; Invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation invocation. RetryCallback<Object, Throwable> retryCallback = new RetryCallback<Object, Throwable>() { public Object doWithRetry(RetryContext context) throws Exception { context.setAttribute(RetryContext.NAME, label); /* * If we don't copy the invocation carefully it won't keep a reference to * the other interceptors in the chain. We don't have a choice here but to * specialise to ReflectiveMethodInvocation (but how often would another * implementation  come along?) . */ if (invocation instanceof ProxyMethodInvocation) { try { return ((ProxyMethodInvocation) invocation).invocableClone().proceed(); } catch (Exception e) { throw e; } catch (Error e) { throw e; } catch (Throwable e) { throw new IllegalStateException(e); } } else { throw new IllegalStateException( "MethodInvocation of the wrong type detected - this should not happen with Spring AOP, " + "so please raise an issue if you see this exception"); }}}; if (recoverer ! = null) { ItemRecovererCallback recoveryCallback = new ItemRecovererCallback( invocation.getArguments(), recoverer); return this.retryOperations.execute(retryCallback, recoveryCallback); } // Finally go to the execute method of retryOperations, which is the RetryTemplate from the previous Builder set. return this.retryOperations.execute(retryCallback); }Copy the code

Whether RetryOperationsInterceptor or StatefulRetryOperationsInterceptor, eventually intercept processing logic or call to RetryTemplate the execute method, from the name also see out, RetryTemplate is a template class that contains retry unification logic. However, I don’t think this RetryTemplate is a very “template” because it doesn’t have much to extend.

Retry logic and policy implementation

Spring Retry, described above, utilizes an AOP proxy to enable the Retry mechanism to “invade” business code. Let’s move on to what the retry logic does. The RetryTemplate doExecute method.

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException { RetryPolicy retryPolicy = this.retryPolicy; BackOffPolicy backOffPolicy = this.backOffPolicy; RetryContext Context = open(retryPolicy, state); if (this.logger.isTraceEnabled()) { this.logger.trace("RetryContext retrieved: " + context); } // Make sure the context is available globally for clients who need // it... RetrySynchronizationManager.register(context); Throwable lastException = null; boolean exhausted = false; Try {// If RetryListener is registered, its open method is called, giving the caller a notification. boolean running = doOpenInterceptors(retryCallback, context); if (! running) { throw new TerminatedRetryException( "Retry terminated abnormally by interceptor before first attempt"); } // Get or Start the backoff context... BackOffContext backOffContext = null; Object resource = context.getAttribute("backOffContext"); if (resource instanceof BackOffContext) { backOffContext = (BackOffContext) resource; } if (backOffContext == null) { backOffContext = backOffPolicy.start(context); if (backOffContext ! = null) { context.setAttribute("backOffContext", backOffContext); }} // call RetryPolicy's canRetry method to determine whether RetryPolicy canRetry. While (canRetry(retryPolicy, context) &&! context.isExhaustedOnly()) { try { if (this.logger.isDebugEnabled()) { this.logger.debug("Retry: count=" + context.getRetryCount()); } // clear the lastException lastException = null; / / doWithRetry method, generally speaking is the original method return retryCallback. DoWithRetry (context); } catch (Throwable e) {// The original method throws an exception lastException = e; RegisterThrowable (retryPolicy, state, context, e); } catch (Exception ex) { throw new TerminatedRetryException("Could not register throwable", ex); } finally {// Call the RetryListener onError method doOnErrorInterceptors(retryCallback, context, e); } if (canRetry(retryPolicy, context) &&! Context. ishaustedonly ()) {try {// If you have retry go backoffpolicy-backoff (backOffContext); } catch (BackOffInterruptedException ex) { lastException = e; // back off was prevented by another thread - fail the retry if (this.logger.isDebugEnabled()) { this.logger .debug("Abort retry because interrupted: count=" + context.getRetryCount()); } throw ex; } } if (this.logger.isDebugEnabled()) { this.logger.debug( "Checking for rethrow: count=" + context.getRetryCount()); } if (shouldRethrow(retryPolicy, context, state)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount()); } throw RetryTemplate.<E>wrapIfNecessary(e); } } /* * A stateful attempt that can retry may rethrow the exception before now, * but if we get this far in a stateful retry there's a reason for it, * like a circuit breaker or a rollback classifier. */ if (state ! = null && context.hasAttribute(GLOBAL_STATE)) { break; } } if (state == null && this.logger.isDebugEnabled()) { this.logger.debug( "Retry failed last attempt: count=" + context.getRetryCount()); } exhausted = true; Return handleRetryExhausted(recoveryCallback, context, state); return handleRetryExhausted(recoveryCallback, context, state) } catch (Throwable e) { throw RetryTemplate.<E>wrapIfNecessary(e); } the finally {/ / close with some close logic (retryPolicy, context, the state, lastException = = null | | exhausted). // Call the close method doCloseInterceptors(retryCallback, context, lastException) for RetryListener; RetrySynchronizationManager.clear(); }}Copy the code

The main core retry logic is the code above, which looks pretty simple. Above, we missed the canRetry method of RetryPolicy and the backOff method of BackOffPolicy, and how these two policies came about. Let’s go back to the getRetryPolicy and getRetryPolicy methods in the getStatelessInterceptor method.

private RetryPolicy getRetryPolicy(Annotation retryable) { Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(retryable); @SuppressWarnings("unchecked") Class<? extends Throwable>[] includes = (Class<? extends Throwable>[]) attrs.get("value"); String exceptionExpression = (String) attrs.get("exceptionExpression"); boolean hasExpression = StringUtils.hasText(exceptionExpression); if (includes.length == 0) { @SuppressWarnings("unchecked") Class<? extends Throwable>[] value = (Class<? extends Throwable>[]) attrs.get("include"); includes = value; } @SuppressWarnings("unchecked") Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("exclude"); Integer maxAttempts = (Integer) attrs.get("maxAttempts"); String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression"); if (StringUtils.hasText(maxAttemptsExpression)) { maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression), PARSER_CONTEXT) .getValue(this.evaluationContext, Integer.class); } if (includes.length == 0 && excludes.length == 0) { SimpleRetryPolicy simple = hasExpression ? new ExpressionRetryPolicy(resolve(exceptionExpression)) .withBeanFactory(this.beanFactory) : new SimpleRetryPolicy(); simple.setMaxAttempts(maxAttempts); return simple; } Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<Class<? extends Throwable>, Boolean>(); for (Class<? extends Throwable> type : includes) { policyMap.put(type, true); } for (Class<? extends Throwable> type : excludes) { policyMap.put(type, false); } boolean retryNotExcluded = includes.length == 0; if (hasExpression) { return new ExpressionRetryPolicy(maxAttempts, policyMap, true, exceptionExpression, retryNotExcluded) .withBeanFactory(this.beanFactory); } else { return new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded); }}Copy the code

Well, the code is not difficult, here is a simple summary of good. The @retryable parameter is used to determine which retry policy (SimpleRetryPolicy or ExpressionRetryPolicy) is used.

private BackOffPolicy getBackoffPolicy(Backoff backoff) { long min = backoff.delay() == 0 ? backoff.value() : backoff.delay(); if (StringUtils.hasText(backoff.delayExpression())) { min = PARSER.parseExpression(resolve(backoff.delayExpression()), PARSER_CONTEXT) .getValue(this.evaluationContext, Long.class); } long max = backoff.maxDelay(); if (StringUtils.hasText(backoff.maxDelayExpression())) { max = PARSER.parseExpression(resolve(backoff.maxDelayExpression()), PARSER_CONTEXT) .getValue(this.evaluationContext, Long.class); } double multiplier = backoff.multiplier(); if (StringUtils.hasText(backoff.multiplierExpression())) { multiplier = PARSER.parseExpression(resolve(backoff.multiplierExpression()), PARSER_CONTEXT) .getValue(this.evaluationContext, Double.class); } if (multiplier > 0) { ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy(); if (backoff.random()) { policy = new ExponentialRandomBackOffPolicy(); } policy.setInitialInterval(min); policy.setMultiplier(multiplier); policy.setMaxInterval(max > min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL); if (this.sleeper ! = null) { policy.setSleeper(this.sleeper); } return policy; } if (max > min) { UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy(); policy.setMinBackOffPeriod(min); policy.setMaxBackOffPeriod(max); if (this.sleeper ! = null) { policy.setSleeper(this.sleeper); } return policy; } FixedBackOffPolicy policy = new FixedBackOffPolicy(); policy.setBackOffPeriod(min); if (this.sleeper ! = null) { policy.setSleeper(this.sleeper); } return policy; }Copy the code

MMM, same taste. Is by @ Backoff annotations of the parameters, to determine the specific use which retreat strategy when it comes to the beginning of the article, is FixedBackOffPolicy or UniformRandomBackOffPolicy etc.

Each RetryPolicy overrides the canRetry method and determines whether it needs to be retried in the RetryTemplate. Let’s look at SimpleRetryPolicy

@Override public boolean canRetry(RetryContext context) { Throwable t = context.getLastThrowable(); / / whether the exception thrown conform to retry abnormal / / also, if more than the number of retry return (t = = null | | retryForException (t)) && context. GetRetryCount () < maxAttempts; }Copy the code

Again, let’s look at the FixedBackOffPolicy’s fallback approach.

Protected void doBackOff () throws BackOffInterruptedException {try {/ / fixed time sleeper is sleep. Sleep (backOffPeriod); } catch (InterruptedException e) { throw new BackOffInterruptedException("Thread interrupted while sleeping", e); }}Copy the code

That’s about the main rationale and logic for retry.

RetryContext

I think it’s important to talk about RetryContext, and look at its inheritance.

[] (Albenw. Making. IO/images/Spri…Implementation Principles of Retry Retry __4.png)

You can see that there is a Context for each policy.

In Spring Retry, virtually every policy is a singleton. My initial instinct was to create a new policy for each method that required retry so that there would be no conflicts between retry policies, but I realized that this would create many more policy objects and increase the user’s burden, which was not a good design. Spring Retry takes a more lightweight approach by creating a new Context object for each method that needs to be retried, and then transferring the Context to the policy for retries. And Spring Retry also caches the Context. This is equivalent to optimizing the retry context.

conclusion

Spring Retry uses an AOP mechanism to Retry intrusions into business code. The RetryTemplate contains the core Retry logic and provides rich Retry and fallback policies.

The resources

www.10tiao.com/html/164/20… www.jianshu.com/p/58e753ca0… Paper. Tuisec. Win/detail / 90 bd…