Copyright belongs to the author, any form of reprint please contact the author to obtain authorization and indicate the source.

There is a function that needs to be implemented in the application: you need to upload data to a remote storage service and do other things if the return processing is successful. This functionality is not complex and consists of two steps: the first step is to call a remote Rest service to upload the data and process the returned results; The second step is to get the result of the first step or catch the exception. If there is an error or exception, retry the upload logic, otherwise continue with the following functional business operations.

Conventional solutions

Try-catch-redo simple retry mode

On the basis of packaging normal upload logic, decide whether to retry by judging returned results or monitoring exceptions. At the same time, in order to solve the invalid execution of immediate retry (assuming that the exception is caused by unstable external execution: network jitter), execute the functional logic again after sleeping for a certain delay time.

public void commonRetry(Map<String, Object> dataMap) throws InterruptedException { 
    Map<String, Object> paramMap = Maps.newHashMap(); 
    paramMap.put("tableName"."creativeTable"); 
    paramMap.put("ds"."20160220"); 
    paramMap.put("dataMap", dataMap); 
    boolean result = false; 
    try { 
      result = uploadToOdps(paramMap); 
      if(! result) { Thread.sleep(1000); uploadToOdps(paramMap); }} catch (Exception e) {thread.sleep (1000); uploadToOdps(paramMap); // Try again}}Copy the code

Try-catch-redo-retry strategy Policy retry mode

The retries may not work. To solve this problem, try increasing the retrycount number of retries and the retry interval to increase the retry validity.

public void commonRetry(Map<String, Object> dataMap) throws InterruptedException { 
    Map<String, Object> paramMap = Maps.newHashMap(); 
    paramMap.put("tableName"."creativeTable"); 
    paramMap.put("ds"."20160220"); 
    paramMap.put("dataMap", dataMap); 
    boolean result = false; 
    try { 
      result = uploadToOdps(paramMap); 
      if(! result) { reuploadToOdps(paramMap,1000L,10); }} catch (Exception e) {reuploadToOdps(paramMap,1000L,10); // Delay multiple retries}}Copy the code

Scheme 1 and scheme 2 is a problem: the normal logic and retry logic strong coupling, retry logic depends greatly on the normal logic execution as a result, the normal logic passive retry trigger, expected results to retry roots often due to the complexity of logical submerged and may lead to subsequent operations to retry logic to solve the problem of what to produce inconsistent understanding. Retry correctness is difficult to guarantee and is detrimental to operation and maintenance because the retry design relies on normal logical exceptions or guesswork at the root of the retry.

Graceful retry scheme attempt

Apply command design patterns to decouple normal and retry logic

The specific definition of command design mode is not elaborated. The main point of this scheme is that the command mode can complete the interface operation logic through the execution object, and at the same time encapsulate the processing retry logic internally, without exposing the implementation details. For the caller, it is to execute the normal logic and achieve the goal of decoupling. (Class diagram structure)

The Retryer responds to and processes the retry logic. The Retryer retries are handled by OdpsRetry, the implementation class of the real IRtry interface. By using the command mode, the separation of normal logic and retry logic is elegantly realized, and the separation of normal logic and retry logic is realized by constructing the role of retries, so that retry has better scalability.

Implement an elegant interface retuning mechanism using Guava Retryer

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 to call logic used is Java. The util. Concurrent. Callable call method. Using Guava Retryer is very simple, just do the following steps:

  1. Maven POM introduced
< guava - retry. Version > 2.0.0 < / guava - retry. Version > < the dependency > < groupId > com. Making. Rholder < / groupId > <artifactId>guava-retrying</artifactId> <version>${guava-retry.version}</version>
</dependency>
Copy the code
  1. Define methods that implement the Callable interface so that Guava Retryer can be called
private static Callable<Boolean> updateReimAgentsCall = new Callable<Boolean>() { @Override public Boolean call() throws  Exception { String url = ConfigureUtil.get(OaConstants.OA_REIM_AGENT); String result = HttpMethod.post(url, new ArrayList<BasicNameValuePair>());if(StringUtils.isEmpty(result)){
          throw new RemoteException("Abnormal interface for obtaining OA reimbursable agent");
       }
       List<OAReimAgents> oaReimAgents = JSON.parseArray(result, OAReimAgents.class);
       if(CollectionUtils.isNotEmpty(oaReimAgents)){
           CacheUtil.put(Constants.REIM_AGENT_KEY,oaReimAgents);
           return true;
       }
       return false; }};Copy the code
  1. Define Retry objects and set policies
Retryer<Boolean> Retryer = RetryerBuilder.<Boolean>newBuilder() // Runtime exceptions and checked exceptions will be retried, but error will not be retried. RetryIfException () / / returnfalseRetryIfResult (Predicates. EqualTo (falseWithWaitStrategy (WaitStrategies. FixedWait (10, TimeUnit. SECONDS)) / / attempts. WithStopStrategy (StopStrategies. StopAfterAttempt (3)). The build (); try { retryer.call(updateReimAgentsCall());The following is an alternative to implementing the Callable interface definition method described in step 2
    //retry.call(() -> { FileUtils.downloadAttachment(projectNo, url, saveDir, fileName);  return true; });
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (RetryException e) {
    logger.error("xxx");
}
Copy the code

In three simple steps you can implement the elegant retuning method using Guava Retryer.

More features

RetryerBuilder is a Factory creator. You can customize the retry source and support multiple retry sources. You can configure the retry times or retry timeout time, and you can configure the waiting interval to create Retryer instance. The retry source of RetryerBuilder supports Exception and custom assertion objects, which are set by retryIfException and retryIfResult. Multiple and compatible objects are supported simultaneously.

  • RetryIfException: Runtime and Checked exceptions will be retried, but error will not be retried.
  • RetryIfRuntimeException: Retries are performed only when runtime exceptions are thrown. Checked and Error exceptions are not retried.
  • RetryIfExceptionOfType: Allows us to retry only when certain exceptions occur, such as NullPointerException and IllegalStateException are Runtime exceptions, including custom errors such as:
Only retry if an error is thrown
retryIfExceptionOfType(Error.class)     
# retry only if the specified exception occurs, e.g.    
retryIfExceptionOfType(IllegalStateException.class)  
retryIfExceptionOfType(NullPointerException.class)  
Or via Predicate
retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),  
                Predicates.instanceOf(IllegalStateException.class))) 
Copy the code

RetryIfResult can specify that your Callable method retries when it returns a value, for example

/ / returnfalseRetry retryIfResult (Predicates. EqualTo (false)) / / ends in _error to retry retryIfResult (Predicates. ContainsPattern ("_error$"))  
Copy the code

If we need to do some extra processing after a retry, such as sending an alarm email, we can use RetryListener. After each retry, Guava-Retrying automatically calls back the listeners we registered. You can also register multiple RetryListeners, which are called in the order they were registered.

import com.github.rholder.retry.Attempt; import com.github.rholder.retry.RetryListener; import java.util.concurrent.ExecutionException; public class MyRetryListener<Boolean> implements RetryListener { @Override public <Boolean> void OnRetry (Attempt<Boolean> Attempt) {// Retry(Attempt<Boolean> Attempt) {system.out.print ("[retry]time="+ attempt.getAttemptNumber()); // Delay from first retry system.out.print (",delay="+ attempt.getDelaySinceFirstAttempt()); Return system.out.print (",hasException=" + attempt.hasException());  
        System.out.print(",hasResult="+ attempt.hasResult()); // What causes the exceptionif (attempt.hasException()) {  
            System.out.print(",causeBy=" + attempt.getExceptionCause().toString());  
        } else{// Return system.out.print (",result="+ attempt.getResult()); } // bad practice: add extra exception handling code try {Boolean result = attempt. Get (); System.out.print(",rude get=" + result);  
        } catch (ExecutionException e) {  
            System.err.println("this attempt produce exception."+ e.getCause().toString()); } System.out.println(); }}Copy the code

Next specify a listener in the Retry object: withRetryListener(new MyRetryListener<>())