Spring AOP - Properly Configuring Retry Advice

为君一笑 提交于 2019-12-12 03:27:38

问题


I am new to Spring AOP and have been experimenting a bit.

I am trying to setup Retry & Rate Limiter through Spring AOP for one of my project. The use case is like this:-

  1. Check if TPS is available. If not, throw ThrottledException
  2. If a ThrottledException is thrown, Retry.

The issue I am running into is: This throttling & retry combo is running into an infinite loop (if TPS = 0). That is, retry is not stopping after 'x' attempts.

My Throttling Interceptor is (at a high level) like this:

@Before("<pointcut>")
public void invoke() throws ThrottlingException {
        if (throttler.isThrottled(throttleKey)) {
            throw new ThrottlingException("Call Throttled");
    }
}

My Retry Interceptor is like this:

@AfterThrowing(pointcut="execution(* com.company.xyz.method())", throwing="exception")
public Object invoke(JoinPoint jp, ThrottlingException exception) throws Throwable {
    return RetryingCallable.newRetryingCallable(new Callable<Object>() {

        @Override
        public Object call() throws Exception {
                MethodSignature  signature = (MethodSignature) p.getSignature();
                Method method = signature.getMethod();
                return method.invoke(jp.getThis(), (Object[]) null);
        }

    }, retryPolicy).call();
}

Here RetryingCallable is a simple implementation (internal library written by someone in my company) that takes in a RetryAdvice and applies that.

My relevant spring-config is as follows:

<bean id="retryInterceptor" class="com.company.xyz.RetryInterceptor">
    <constructor-arg index="0"><ref bean="retryPolicy"/></constructor-arg> 
</bean>


<bean id="throttlingInterceptor" class="com.company.xyz.ThrottlingInterceptor">
    <constructor-arg><value>throttleKey</value></constructor-arg> 
</bean>
<context:component-scan base-package="com.company.xyz">
  <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>
<aop:aspectj-autoproxy/>

The issue here, as I see, is that on each ThrottlingException a new Retry Advice is being applied instead of the previous one being coming into affect.

Any inputs on how to fix this?


回答1:


Disclaimer: I am not a Spring user, thus I am going to present a pure AspectJ solution here. It should work the same way in Spring AOP though. The only thing you need to change is switch from @DeclarePresedence to @Order for aspect precedence configuration as described in the Spring AOP manual.

Driver application:

package de.scrum_master.app;

public class Application {
    public static void main(String[] args) {
        new Application().doSomething();
    }

    public void doSomething() {
        System.out.println("Doing something");
    }
}

Throttling exception class:

package de.scrum_master.app;

public class ThrottlingException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public ThrottlingException(String arg0) {
        super(arg0);
    }
}

Throttling interceptor:

In order to emulate the throttling situation I created a helper method isThrottled() which returns true randomly in 2 out of 3 cases.

package de.scrum_master.aspect;

import java.util.Random;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import de.scrum_master.app.ThrottlingException;

@Aspect
public class ThrottlingInterceptor {
    private static final Random RANDOM = new Random();

    @Before("execution(* doSomething())")
    public void invoke(JoinPoint thisJoinPoint) throws ThrottlingException {
        System.out.println(getClass().getSimpleName() + " -> " + thisJoinPoint);
        if (isThrottled()) {
            throw new ThrottlingException("call throttled");
        }
    }

    private boolean isThrottled() {
        return RANDOM.nextInt(3) > 0;
    }
}

Retry interceptor:

Please note that the AspectJ annotation @DeclarePrecedence("RetryInterceptor, *") says that this interceptor is to be executed before any other ones. Please replace it with @Order annotations on both interceptor classes. Otherwise the @Around advice cannot catch exceptions thrown by the throttling interceptor.

Also worth mentioning is that this interceptor does not need any reflection in order to implement the retry logic, it directly uses the joinpoint within a retry loop in order to retry thisJoinPoint.proceed(). This can easily be factored out into a helper method or helper class implementing different kinds of retry behaviour. Just make sure to use the ProceedingJoinPoint as a parameter instead of a Callable.

package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclarePrecedence;

import de.scrum_master.app.ThrottlingException;

@Aspect
@DeclarePrecedence("RetryInterceptor, *")
public class RetryInterceptor {
    private static int MAX_TRIES = 5;
    private static int WAIT_MILLIS_BETWEEN_TRIES = 1000;

    @Around("execution(* doSomething())")
    public Object invoke(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        System.out.println(getClass().getSimpleName() + " -> " + thisJoinPoint);
        ThrottlingException throttlingException = null;
        for (int i = 1; i <= MAX_TRIES; i++) {
            try {
                return thisJoinPoint.proceed();
            }
            catch (ThrottlingException e) {
                throttlingException = e;
                System.out.println("  Throttled during try #" + i);
                Thread.sleep(WAIT_MILLIS_BETWEEN_TRIES);
            }
        }
        throw throttlingException;
    }
}

Console log for successful retry:

RetryInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #1
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #2
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Doing something

Console log for failed retry:

RetryInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #1
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #2
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #3
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #4
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
  Throttled during try #5
Exception in thread "main" de.scrum_master.app.ThrottlingException: call throttled
    at de.scrum_master.aspect.ThrottlingInterceptor.invoke(ThrottlingInterceptor.aj:19)
    at de.scrum_master.app.Application.doSomething_aroundBody0(Application.java:9)
    at de.scrum_master.app.Application.doSomething_aroundBody1$advice(Application.java:22)
    at de.scrum_master.app.Application.doSomething(Application.java:1)
    at de.scrum_master.app.Application.main(Application.java:5)

Feel free to ask any follow-up questions related to my answer.


Update: I have no idea how your RetryingCallable and RetryPolicy classes/interfaces work, you did not tell me much about it. But I made up something and got it working like this:

package de.scrum_master.app;

import java.util.concurrent.Callable;

public interface RetryPolicy<V> {
    V apply(Callable<V> callable) throws Exception;
}
package de.scrum_master.app;

import java.util.concurrent.Callable;

public class DefaultRetryPolicy<V> implements RetryPolicy<V> {
    private static int MAX_TRIES = 5;
    private static int WAIT_MILLIS_BETWEEN_TRIES = 1000;

    @Override
    public V apply(Callable<V> callable) throws Exception {
        Exception throttlingException = null;
        for (int i = 1; i <= MAX_TRIES; i++) {
            try {
                return callable.call();
            }
            catch (ThrottlingException e) {
                throttlingException = e;
                System.out.println("  Throttled during try #" + i);
                Thread.sleep(WAIT_MILLIS_BETWEEN_TRIES);
            }
        }
        throw throttlingException;
    }
}
package de.scrum_master.app;

import java.util.concurrent.Callable;

public class RetryingCallable<V> {
    private RetryPolicy<V> retryPolicy;
    private Callable<V> callable;

    public RetryingCallable(Callable<V> callable, RetryPolicy<V> retryPolicy) {
        this.callable = callable;
        this.retryPolicy = retryPolicy;
    }

    public static <V> RetryingCallable<V> newRetryingCallable(Callable<V> callable, RetryPolicy<V> retryPolicy) {
        return new RetryingCallable<V>(callable, retryPolicy);
    }

    public V call() throws Exception {
        return retryPolicy.apply(callable);
    }
}

Now change the retry interceptor like this:

package de.scrum_master.aspect;

import java.util.concurrent.Callable;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclarePrecedence;

import de.scrum_master.app.DefaultRetryPolicy;
import de.scrum_master.app.RetryPolicy;
import de.scrum_master.app.RetryingCallable;

@Aspect
@DeclarePrecedence("RetryInterceptor, *")
public class RetryInterceptor {
    private RetryPolicy<Object> retryPolicy = new DefaultRetryPolicy<>();

    @Around("execution(* doSomething())")
    public Object invoke(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        System.out.println(getClass().getSimpleName() + " -> " + thisJoinPoint);
        return RetryingCallable.newRetryingCallable(
            new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    return thisJoinPoint.proceed();
                }
            },
            retryPolicy
        ).call();
    }
}

The log output will be pretty much similar to what you saw before. For me this works nicely.



来源:https://stackoverflow.com/questions/36811268/spring-aop-properly-configuring-retry-advice

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!