View Javadoc
1   /*
2    * Copyright (c) 2012-2024, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.aspects.aj;
31  
32  import com.jcabi.aspects.Immutable;
33  import com.jcabi.aspects.RetryOnFailure;
34  import com.jcabi.log.Logger;
35  import java.lang.reflect.Method;
36  import java.security.SecureRandom;
37  import java.util.Random;
38  import org.aspectj.lang.ProceedingJoinPoint;
39  import org.aspectj.lang.annotation.Around;
40  import org.aspectj.lang.annotation.Aspect;
41  import org.aspectj.lang.reflect.MethodSignature;
42  
43  /**
44   * Repeat execution in case of exception.
45   *
46   * @since 0.1.10
47   * @see RetryOnFailure
48   */
49  @Aspect
50  @Immutable
51  public final class Repeater {
52  
53      /**
54       * Pseudo random number generator.
55       */
56      private static final Random RAND = new SecureRandom();
57  
58      /**
59       * Catch exception and re-call the method.
60       * @param point Joint point
61       * @return The result of call
62       * @throws Throwable If something goes wrong inside
63       * @checkstyle IllegalThrows (7 lines)
64       * @checkstyle LineLength (4 lines)
65       * @checkstyle NonStaticMethodCheck (100 lines)
66       * @checkstyle ExecutableStatementCountCheck (100 lines)
67       */
68      @Around("execution(* * (..)) && @annotation(com.jcabi.aspects.RetryOnFailure)")
69      @SuppressWarnings({ "PMD.AvoidCatchingThrowable", "PMD.GuardLogStatement" })
70      public Object wrap(final ProceedingJoinPoint point) throws Throwable {
71          final Method method = ((MethodSignature) point.getSignature())
72              .getMethod();
73          final RetryOnFailure rof = method.getAnnotation(RetryOnFailure.class);
74          int attempt = 0;
75          final long begin = System.nanoTime();
76          final Class<? extends Throwable>[] types = rof.types();
77          final ImprovedJoinPoint joinpoint = new ImprovedJoinPoint(point);
78          while (true) {
79              final long start = System.nanoTime();
80              try {
81                  return point.proceed();
82              } catch (final InterruptedException ex) {
83                  Thread.currentThread().interrupt();
84                  throw ex;
85                  // @checkstyle IllegalCatch (1 line)
86              } catch (final Throwable ex) {
87                  if (Repeater.matches(ex.getClass(), rof.ignore())) {
88                      throw ex;
89                  }
90                  if (!Repeater.matches(ex.getClass(), types)) {
91                      throw ex;
92                  }
93                  ++attempt;
94                  if (Logger.isWarnEnabled(joinpoint.targetize())) {
95                      if (rof.verbose()) {
96                          Logger.warn(
97                              joinpoint.targetize(),
98                              // @checkstyle LineLength (1 line)
99                              "#%s(): attempt #%d of %d failed in %[nano]s (%[nano]s waiting already) with %[exception]s",
100                             method.getName(),
101                             attempt, rof.attempts(), System.nanoTime() - start,
102                             System.nanoTime() - begin, ex
103                         );
104                     } else {
105                         Logger.warn(
106                             joinpoint.targetize(),
107                             // @checkstyle LineLength (1 line)
108                             "#%s(): attempt #%d/%d failed with %[type]s in %[nano]s (%[nano]s in total): %s",
109                             method.getName(),
110                             attempt, rof.attempts(), ex, System.nanoTime() - start,
111                             System.nanoTime() - begin,
112                             Repeater.message(ex)
113                         );
114                     }
115                 }
116                 if (attempt >= rof.attempts()) {
117                     throw ex;
118                 }
119                 if (rof.delay() > 0L) {
120                     this.delay(rof, attempt);
121                 }
122             }
123         }
124     }
125 
126     /**
127      * Waits certain time before returning.
128      * @param rof RetryOnFailure parameters.
129      * @param attempt Attempt number.
130      * @throws InterruptedException If wait has been interrupted.
131      */
132     private void delay(final RetryOnFailure rof, final int attempt) throws
133         InterruptedException {
134         final long delay;
135         if (rof.randomize()) {
136             delay = (long) Repeater.RAND.nextInt(2 << attempt) * rof.delay();
137         } else {
138             delay = rof.delay() * (long) attempt;
139         }
140         rof.unit().sleep(delay);
141     }
142 
143     /**
144      * Get a message out of a potentially chained exception (recursively
145      * calls itself in order to reproduce a chain of messages).
146      * @param exp The exception
147      * @return The message
148      */
149     private static String message(final Throwable exp) {
150         final StringBuilder text = new StringBuilder(0);
151         text.append(exp.getMessage());
152         if (exp.getCause() != null) {
153             text.append("; ").append(Repeater.message(exp.getCause()));
154         }
155         String msg = text.toString();
156         if (msg.length() > 100) {
157             msg = String.format("%s...", msg.substring(0, 100));
158         }
159         return msg;
160     }
161 
162     /**
163      * Checks if the exception thrown matches the list.
164      * @param thrown The thrown exception class
165      * @param types The exceptions to match
166      * @return TRUE if matches
167      */
168     @SafeVarargs
169     private static boolean matches(
170         final Class<? extends Throwable> thrown,
171         final Class<? extends Throwable>... types
172     ) {
173         boolean matches = false;
174         for (final Class<? extends Throwable> type : types) {
175             if (type.isAssignableFrom(thrown)) {
176                 matches = true;
177                 break;
178             }
179         }
180         return matches;
181     }
182 
183 }