I’m encountering a problem with a custom RetryAspect implementation using Java AOP, where the retry logic exceeds the maximum attempts configured. Here’s the scenario and the current implementation details:
Scenario:
I have implemented a custom RetryAspect
to retry methods annotated with @Retryable
. The aspect is supposed to retry the method execution a maximum of 3 times (maxAttempts = 3
) if certain exceptions are thrown.
Code Details:
Here’s my RetryAspect
implementation:
<code> import com.annotations.Retryable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
public class RetryAspect {
final Object lock = new Object();
@Pointcut("@annotation(retryable)")
public void retryableMethods(Retryable retryable) {
@Around(value = "retryableMethods(retryable)", argNames = "joinPoint,retryable")
public Object retryMethod(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable {
int maxAttempts = retryable.maxAttempts();
long delay = retryable.backoff().delay();
Throwable lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
System.out.println("Attempt " + attempt + " of " + maxAttempts);
return joinPoint.proceed();
System.out.println("Caught exception: " + ex.getClass().getSimpleName());
if (isExceptionIncluded(ex, retryable.include())) {
if (attempt < maxAttempts) {
System.out.println("Sleeping for " + delay + "ms before retry");
System.out.println("Exception " + ex.getClass().getSimpleName() + " is not included in retryable.include(). Rethrowing...");
throw ex; // Rethrow the exception since it's not retryable
// If we reach here, maxAttempts is exceeded
System.out.println("Max attempts exceeded (" + maxAttempts + "). Throwing last exception.");
throw lastException != null ? lastException : new RuntimeException("Retry failed after max attempts");
private boolean isExceptionIncluded(Throwable ex, Class<? extends Throwable>[] includedClasses) {
for (Class<? extends Throwable> includedClass : includedClasses) {
if (includedClass.isInstance(ex)) {
<code> import com.annotations.Retryable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class RetryAspect {
final Object lock = new Object();
@Pointcut("@annotation(retryable)")
public void retryableMethods(Retryable retryable) {
}
@Around(value = "retryableMethods(retryable)", argNames = "joinPoint,retryable")
public Object retryMethod(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable {
int maxAttempts = retryable.maxAttempts();
long delay = retryable.backoff().delay();
Throwable lastException = null;
synchronized (lock) {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
System.out.println("Attempt " + attempt + " of " + maxAttempts);
return joinPoint.proceed();
} catch (Throwable ex) {
System.out.println("Caught exception: " + ex.getClass().getSimpleName());
if (isExceptionIncluded(ex, retryable.include())) {
lastException = ex;
if (attempt < maxAttempts) {
System.out.println("Sleeping for " + delay + "ms before retry");
Thread.sleep(delay);
}
} else {
System.out.println("Exception " + ex.getClass().getSimpleName() + " is not included in retryable.include(). Rethrowing...");
throw ex; // Rethrow the exception since it's not retryable
}
}
}
// If we reach here, maxAttempts is exceeded
System.out.println("Max attempts exceeded (" + maxAttempts + "). Throwing last exception.");
throw lastException != null ? lastException : new RuntimeException("Retry failed after max attempts");
}
}
private boolean isExceptionIncluded(Throwable ex, Class<? extends Throwable>[] includedClasses) {
for (Class<? extends Throwable> includedClass : includedClasses) {
if (includedClass.isInstance(ex)) {
return true;
}
}
return false;
}
}
</code>
import com.annotations.Retryable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class RetryAspect {
final Object lock = new Object();
@Pointcut("@annotation(retryable)")
public void retryableMethods(Retryable retryable) {
}
@Around(value = "retryableMethods(retryable)", argNames = "joinPoint,retryable")
public Object retryMethod(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable {
int maxAttempts = retryable.maxAttempts();
long delay = retryable.backoff().delay();
Throwable lastException = null;
synchronized (lock) {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
System.out.println("Attempt " + attempt + " of " + maxAttempts);
return joinPoint.proceed();
} catch (Throwable ex) {
System.out.println("Caught exception: " + ex.getClass().getSimpleName());
if (isExceptionIncluded(ex, retryable.include())) {
lastException = ex;
if (attempt < maxAttempts) {
System.out.println("Sleeping for " + delay + "ms before retry");
Thread.sleep(delay);
}
} else {
System.out.println("Exception " + ex.getClass().getSimpleName() + " is not included in retryable.include(). Rethrowing...");
throw ex; // Rethrow the exception since it's not retryable
}
}
}
// If we reach here, maxAttempts is exceeded
System.out.println("Max attempts exceeded (" + maxAttempts + "). Throwing last exception.");
throw lastException != null ? lastException : new RuntimeException("Retry failed after max attempts");
}
}
private boolean isExceptionIncluded(Throwable ex, Class<? extends Throwable>[] includedClasses) {
for (Class<? extends Throwable> includedClass : includedClasses) {
if (includedClass.isInstance(ex)) {
return true;
}
}
return false;
}
}
Here’s my ExampleService
implementation:
<code>public class ExampleService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), include = RetryException.class)
public void retryableMethod() throws RetryException {
System.out.println("Executing retryableMethod...");
throw new RetryException("Simulated Exception");
<code>public class ExampleService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), include = RetryException.class)
public void retryableMethod() throws RetryException {
System.out.println("Executing retryableMethod...");
throw new RetryException("Simulated Exception");
}
}
</code>
public class ExampleService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), include = RetryException.class)
public void retryableMethod() throws RetryException {
System.out.println("Executing retryableMethod...");
throw new RetryException("Simulated Exception");
}
}
Problem:
However, during testing, I noticed that the retry logic continues beyond 3 attempts. Here’s the output I’m observing:
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
<code>Attempt 1 of 3
Attempt 1 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Attempt 1 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Attempt 1 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
</code>
Attempt 1 of 3
Attempt 1 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Attempt 1 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Attempt 1 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 2 of 3
Executing retryableMethod...
Caught exception: RetryException
Sleeping for 500ms before retry
Attempt 3 of 3
Executing retryableMethod...
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Caught exception: RetryException
Max attempts exceeded (3). Throwing last exception.
Expected Behavior:
The retry logic should attempt the method execution up to 3 times and throw the last exception when maxAttempts
is reached. However, it currently retries beyond the configured limit.
Additional Information:
Java version: 17
I’ve tried adjusting the loop structure and ensuring proper exception handling, but I haven’t been successful in limiting the retry attempts. Any insights or suggestions on how to correct this behavior would be greatly appreciated. Thank you!