Sisyphus combines the benefits of Spring-Retry and Gauva-Retrying and is flexible to use.

Today, let’s take a look at the story behind Sisyphus.

Scene import

Simple requirements

Product manager: to achieve a conditional, query user information service.

Xiaoming: Ok. no problem

code

  • UserService.java
public interface UserService {

    /** * Query user information based on conditions *@paramCondition conditions *@return* / User information
    User queryUser(QueryUserCondition condition);

}
Copy the code
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    private OutService outService;

    public UserServiceImpl(OutService outService) {
        this.outService = outService;
    }

    @Override
    public User queryUser(QueryUserCondition condition) {
        outService.remoteCall();
        return newUser(); }}Copy the code

The conversation

Project manager: This service sometimes fails. Take a look.

Xiaoming: OutService is an external RPC service, but sometimes unstable.

Project Manager: If the call fails, you can retry the call several times. You take a look at retries

retry

Retry role

Retry is not suitable for all scenarios, such as invalid parameter verification and write operation (considering whether the write is idempotent or not).

Remote call timeout, sudden network interruption can be retried. Microservice 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.

For example, if an external RPC call or data entry fails, you can retry the call multiple times to improve the likelihood of success.

V1.0 supports retry versions

thinking

Xiaoming: I have other tasks at hand. That’s easy too. Five minutes to take him down.

implementation

  • UserServiceRetryImpl.java
public class UserServiceRetryImpl implements UserService {

    @Override
    public User queryUser(QueryUserCondition condition) {
        int times = 0;
        OutService outService = new AlwaysFailOutServiceImpl();

        while (times < RetryConstant.MAX_TIMES) {
            try {
                outService.remoteCall();
                return new User();
            } catch (Exception e) {
                times++;

                if(times >= RetryConstant.MAX_TIMES) {
                    throw newRuntimeException(e); }}}return null; }}Copy the code

V1.1 Proxy mode version

Easy to maintain

Project manager: I have seen your code, although the function has been implemented, but try to write a little easier to maintain.

Xiaoming: Ok. (Thinking, do you mean to write some notes or something?)

The proxy pattern

Provide a proxy for other objects to control access to that object.

In some cases, an object is inappropriate or cannot directly reference another object, and a proxy object can act as an intermediary between a client and a target object.

It is characterized by the fact that the proxy and delegate classes have the same interface.

implementation

Xiaoming thought of the proxy model that he had seen before, and thought that in this way, the original code would change less momentum, and it would be more convenient to change it later.

  • UserServiceProxyImpl.java
public class UserServiceProxyImpl implements UserService {

    private UserService userService = new UserServiceImpl();

    @Override
    public User queryUser(QueryUserCondition condition) {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                return userService.queryUser(condition);
            } catch (Exception e) {
                times++;

                if(times >= RetryConstant.MAX_TIMES) {
                    throw newRuntimeException(e); }}}return null; }}Copy the code

V1.2 Dynamic proxy mode

Convenient to expand

Project Manager: Xiao Ming, here’s another method with the same problem. You can also add retry.

Xiaoming: Ok.

Xiao Ming thought, I am writing an agent, but then calmed down, what if there is another service to try again?

  • RoleService.java
public interface RoleService {

    /** * query *@paramUser User information *@returnWhether you have permission */
    boolean hasPrivilege(User user);

}
Copy the code

Code implementation

  • DynamicProxy.java
public class DynamicProxy implements InvocationHandler {

    private final Object subject;

    public DynamicProxy(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                // When a proxy object calls a method of the real object, it automatically jumps to the invoke method of the handler object associated with the proxy object
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;

                if (times >= RetryConstant.MAX_TIMES) {
                    throw newRuntimeException(e); }}}return null;
    }

    /** * get the dynamic proxy **@paramRealSubject Proxy object */
    public static Object getProxy(Object realSubject) {
        // The real object we want to delegate is passed in, and its methods are finally called through the real object
        InvocationHandler handler = new DynamicProxy(realSubject);
        returnProxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler); }}Copy the code
  • The test code
@Test
public void failUserServiceTest(a) {
        UserService realService = new UserServiceImpl();
        UserService proxyService = (UserService) DynamicProxy.getProxy(realService);

        User user = proxyService.queryUser(new QueryUserCondition());
        LOGGER.info("failUserServiceTest: " + user);
}


@Test
public void roleServiceTest(a) {
        RoleService realService = new RoleServiceImpl();
        RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService);

        boolean hasPrivilege = proxyService.hasPrivilege(new User());
        LOGGER.info("roleServiceTest: " + hasPrivilege);
}
Copy the code

V1.3 Dynamic proxy mode enhanced

dialogue

Project manager: Xiao Ming, your dynamic proxy approach is lazy, but some of our classes don’t have interfaces. This is a problem you need to solve.

Xiaoming: Ok. (who? Write service does not define interface.

  • ResourceServiceImpl.java
public class ResourceServiceImpl {

    /** * Verify resource information *@paramThe user into * refs@returnCheck whether */ passes the verification
    public boolean checkResource(User user) {
        OutService outService = new AlwaysFailOutServiceImpl();
        outService.remoteCall();
        return true; }}Copy the code

Bytecode technology

Xiaoming looked at the information on the net, the solution is still some.

  • CGLIB

CGLIB is a powerful, high-performance, and high-quality code generation library for extending JAVA classes and implementing interfaces at run time.

  • javassist

Javassist (Java Programming Assistant) makes Java bytecode manipulation easy.

It is a Java class library for editing bytecodes; It allows Java programs to define new classes at run time and modify class files when the JVM loads them.

Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level.

If users use source-level apis, they can edit class files without having to understand Java bytecode specifications.

The entire API is designed using only the Java language vocabulary. You can even specify the inserted bytecode as source text; Javassist compiles it dynamically.

Bytecode level apis, on the other hand, allow users to edit class files directly as other editors.

  • ASM

ASM is a general-purpose Java bytecode manipulation and analysis framework.

It can be used to modify existing classes or generate classes dynamically, directly in binary form.

ASM provides some generic bytecode conversion and analysis algorithms from which you can build custom complex conversion and code analysis tools.

ASM provides similar functionality to other Java bytecode frameworks, but focuses on performance.

Because it is designed and implemented as small and fast as possible, it is ideal for use in dynamic systems (and, of course, statically, for example, in compilers).

implementation

Xiao Ming took a look and chose to use CGLIB.

  • CglibProxy.java
public class CglibProxy implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                // Call the methods of the parent class through the proxy subclass
                return methodProxy.invokeSuper(o, objects);
            } catch (Exception e) {
                times++;

                if (times >= RetryConstant.MAX_TIMES) {
                    throw newRuntimeException(e); }}}return null;
    }

    /** * get the proxy class *@paramClazz class information *@returnProxy class result */
    public Object getProxy(Class clazz){
        Enhancer enhancer = new Enhancer();
        // Target object class
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        // Use bytecode technology to create a subclass instance of the target object class as a proxy
        returnenhancer.create(); }}Copy the code
  • test
@Test
public void failUserServiceTest(a) {
   UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);

   User user = proxyService.queryUser(new QueryUserCondition());
   LOGGER.info("failUserServiceTest: " + user);
}

@Test
public void resourceServiceTest(a) {
   ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
   boolean result = proxyService.checkResource(new User());
   LOGGER.info("resourceServiceTest: " + result);
}
Copy the code

V2.0 AOP implementation

dialogue

Project Manager: Xiao Ming, I’ve been thinking about a problem recently. The number of retries should be different for different services. Because services have different requirements for stability.

Xiaoming: Ok. (Thinking, it’s Friday after a week of retries.)

Xiao Ming was thinking about this problem until he got off work. It’s the weekend, so take some time to write a retry widget.

Design ideas

  • Technical support

spring

Java annotations

  • Annotations to define

Annotations can be used on methods to define the number of retries required

  • Annotations parsing

Interception specifies the method that needs to be retried, resolves the corresponding number of retries, and then performs the corresponding number of retries.

implementation

  • Retryable.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {

    /**
     * Exception type that are retryable.
     * @return exception type to retry
     */
    Class<? extends Throwable> value() default RuntimeException.class;

    /** * contains the first failure *@return the maximum number of attempts (including the first failure), defaults to 3
     */
    int maxAttempts(a) default 3;

}
Copy the code
  • RetryAspect.java
@Aspect
@Component
public class RetryAspect {

    @Pointcut("execution(public * com.github.houbb.retry.aop.. *. * (..) ) &&" + "@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
    public void myPointcut(a) {}@Around("myPointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Method method = getCurrentMethod(point);
        Retryable retryable = method.getAnnotation(Retryable.class);

        //1
        int maxAttempts = retryable.maxAttempts();
        if (maxAttempts <= 1) {
            return point.proceed();
        }

        //2. Exception handling
        int times = 0;
        final Class<? extends Throwable> exceptionClass = retryable.value();
        while (times < maxAttempts) {
            try {
                return point.proceed();
            } catch (Throwable e) {
                times++;

                // The number of retries exceeds the maximum or is not an exception
                if(times >= maxAttempts || ! e.getClass().isAssignableFrom(exceptionClass)) {throw newThrowable(e); }}}return null;
    }

    private Method getCurrentMethod(ProceedingJoinPoint point) {
        try {
            Signature sig = point.getSignature();
            MethodSignature msig = (MethodSignature) sig;
            Object target = point.getTarget();
            return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw newRuntimeException(e); }}}Copy the code

Use of methods

  • fiveTimes()

The current method is retried five times. Retry condition: The service throws AopRuntimeExption

@Override @Retryable(maxAttempts = 5, value = AopRuntimeExption.class) public void fiveTimes() { LOGGER.info("fiveTimes called!" ); throw new AopRuntimeExption(); }Copy the code
  • The test log
The 2018-08-08 15:49:33. 814 INFO [main] com. Making. Houbb. Retry. Aop) service. Impl. UserServiceImpl: 66 - fiveTimes called! The 2018-08-08 15:49:33. 815 INFO [main] com. Making. Houbb. Retry. Aop) service. Impl. UserServiceImpl: 66 - fiveTimes called! The 2018-08-08 15:49:33. 815 INFO [main] com. Making. Houbb. Retry. Aop) service. Impl. UserServiceImpl: 66 - fiveTimes called! The 2018-08-08 15:49:33. 815 INFO [main] com. Making. Houbb. Retry. Aop) service. Impl. UserServiceImpl: 66 - fiveTimes called! The 2018-08-08 15:49:33. 815 INFO [main] com. Making. Houbb. Retry. Aop) service. Impl. UserServiceImpl: 66 - fiveTimes called! java.lang.reflect.UndeclaredThrowableException ...Copy the code

V3.0 spring – retry version

dialogue

Coming to the company on Monday, the project manager talked with Xiao Ming again.

Project manager: The number of retries is sufficient, but retries should be strategic. For example, if the external call fails for the first time, it can wait 5 seconds for the next call. If it fails again, it can wait 10 seconds for the call.

Xiaoming: I see.

thinking

But it’s Monday, and I have a lot of other things to do.

Xiao Ming is thinking, no time to write this ah. See if you can find one online.

spring-retry

Spring Retry provides declarative Retry support for Spring applications. It is used for Spring for Spring batch processing, Spring integration, Apache Hadoop(and so on).

In a distributed system, to ensure the consistency of distributed data transactions, a retry operation is performed when the network jitter request may time out when the RPC interface is invoked or MQ is sent. MQ is the most popular way to retry, but if MQ is not introduced in your project, it is inconvenient.

Alternatively, developers can write their own retry mechanisms, but most are less elegant.

Annotated use

  • RemoteService.java

Retry condition: RuntimeException encountered

Retry times: 3

Retry policy: Wait 5 seconds after the retry, and the subsequent times will be two times of the original time.

Circuit breaker: If all retries fail, the recover() method is called.

@Service
public class RemoteService {

    private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);

    /** * calls the method */
    @Retryable(value = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(delay = 5000L, multiplier = 2))
    public void call(a) {
        LOGGER.info("Call something...");
        throw new RuntimeException("RPC call exception");
    }

    /** * recover mechanism *@paramE * /
    @Recover
    public void recover(RuntimeException e) {
        LOGGER.info("Start do recover things....");
        LOGGER.warn("We meet ex: ", e); }}Copy the code
  • test
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest {

    @Autowired
    private RemoteService remoteService;

    @Test
    public void test(a) { remoteService.call(); }}Copy the code
  • The log
The 2018-08-08 16:03:26. 1433-409 the INFO [main] C.G.H.R.S pring. Service. RemoteService: Call something... The 2018-08-08 16:03:31. 1433-414 the INFO [main] C.G.H.R.S pring. Service. RemoteService: Call something... The 2018-08-08 16:03:41. 1433-416 the INFO [main] C.G.H.R.S pring. Service. RemoteService: Call something... The 16:03:41 2018-08-08. 1433-418 the INFO [main] C.G.H.R.S pring. Service. RemoteService: Start the do recover things... The 2018-08-08 16:03:41. 1433-425 WARN [main] C.G.H.R.S pring. Service. RemoteService: We meet the ex: java.lang.RuntimeException: RPC calls anomalies at com. Making. Houbb. Retry. Spring. Service. RemoteService. Call (RemoteService. Java: 38) ~ / classes / : na...Copy the code

Time points of the three calls:

2018-08-08 16:03:26.409 
2018-08-08 16:03:31.414
2018-08-08 16:03:41.416
Copy the code

defects

The Spring-Retry tool, while elegant, has two unfriendly designs:

One is that the retry entity is restricted to a Throwable subclass, indicating that retries are designed for catching functional exceptions, but we want to rely on a data object entity as a retry entity, but the Sping-Retry framework must cast it to a Throwable subclass.

Another is that the assertion object at the retry root uses a doWithRetry Exception instance, which does not conform to the return design of normal internal assertions.

Spring Retry advocates annotated retries of methods. The Retry logic is executed synchronously, and the “failure” of retries is Throwable. If you are trying to determine whether a Retry is needed based on the state of the returned value, you may have to judge the returned value yourself and then explicitly throw an exception.

The @Recover annotation cannot specify methods when used, which can be troublesome if there are multiple retry methods in a class.

guava-retrying

The conversation

Xiao Hua: Our system also uses retry

Project Manager: Xiaoming used Spring-retry some time ago. It would be good to share it

Xiaoming: Spring-Retry has all the basic functions, but it must be controlled based on exceptions. If you are trying to determine whether a retry is needed based on the status of a returned value, you might have to determine the returned value yourself and explicitly throw an exception.

Xiao Hua: In our project, we want to retry according to the properties of the object. You can look at Guava-retry. I used it a long time ago and it looks good.

Xiaoming: Ok.

guava-retrying

The Guava-Retrying module provides a generic way to retry arbitrary Java code using guava predicate matching enhanced specific stop, retry, and exception-handling capabilities.

  • advantage

The Guava Retryer tool is similar to Spring-Retry in that it packages logical retries by defining the retries role. However, the Guava Retryer has a better policy definition. In addition to controlling the number of retries and retry frequency, the Guava Retryer tool is compatible with retry source definitions that support multiple exceptions or user-defined entity objects. Give the retry feature more flexibility.

Guava Retryer is thread-safe, entrance call logic USES Java. Util. Concurrent. Callable call () method

The code example

An introduction to case

When an exception is encountered, retry three times to stop

  • HelloDemo.java
public static void main(String[] args) {
    Callable<Boolean> callable = new Callable<Boolean>() {
        @Override
        public Boolean call(a) throws Exception {
            // do something useful here
            LOGGER.info("call...");
            throw newRuntimeException(); }}; Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() .retryIfResult(Predicates.isNull()) .retryIfExceptionOfType(IOException.class) .retryIfRuntimeException() .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            .build();
    try {
        retryer.call(callable);
    } catch(RetryException | ExecutionException e) { e.printStackTrace(); }}Copy the code
  • The log
The 2018-08-08 17:21:12. 442 INFO [main] com. Making. Houbb. Retry. Guava. HelloDemo: 41 - call... com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts. 2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call... The 2018-08-08 17:21:12. 444 INFO [main] com. Making. Houbb. Retry. Guava. HelloDemo: 41 - call... at com.github.rholder.retry.Retryer.call(Retryer.java:174) at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53) Caused by: java.lang.RuntimeException at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42) at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37) at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78) at com.github.rholder.retry.Retryer.call(Retryer.java:160) ... 1 moreCopy the code

conclusion

The commonality and principle of elegant retry

Normal and retry are elegantly decoupled, and retry assertion condition instances or logical exception instances are the medium through which the two communicate.

Specify the retry interval, configure different retry policies, and set the retry timeout period to further ensure the retry validity and stability of the retry process.

Both use the command design mode, through the delegate retry object to complete the corresponding logical operation, at the same time internal encapsulation to achieve the retry logic.

Spring-retry and Guava-Retry tools are thread-safe retries that support logical retry in concurrent service scenarios.

Graceful retry applies to scenarios

Unstable dependency scenarios exist in the functional logic. Retry is required to obtain the expected result or re-execution of the logic does not end immediately. Such as remote interface access, data load access, data upload verification and so on.

For exception scenarios, retry is required and normal logic and retry logic are decoupled.

Retry schemes can also be considered for scenarios that require data-media-based interactions and wish to test execution logic through retry polling.

The conversation

Project Manager: I think Guava-Retry is good, but not convenient enough. Xiao Ming, why don’t you wrap one based on annotations?

Xiao Ming:…

Better implementation

So Ming wrote Sisyphus with tears in his eyes.

Java Retry framework – Sisyphus

I hope this article is helpful to you. If you like it, please click to collect and forward a wave.

I am old ma, looking forward to meeting with you next time.