Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BackOffByExceptionTypePolicy to backoff by exception type #400

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.springframework.retry.backoff;

import org.springframework.classify.Classifier;
import org.springframework.classify.ClassifierSupport;
import org.springframework.classify.SubclassClassifier;
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.ExceptionClassifierRetryPolicy;

import java.util.HashMap;
import java.util.Map;

/**
* A {@link BackOffPolicy} that dynamically adapts to one of a set of injected policies
* according to the value of the latest exception. Modelled after
* {@link ExceptionClassifierRetryPolicy}
*
*/

public class BackOffByExceptionTypePolicy implements BackOffPolicy {

private Classifier<Throwable, BackOffPolicy> throwableToBackOffPolicyClassifier = new ClassifierSupport<>(
new NoBackOffPolicy()); // defaults to NoBackOffPolicy

/**
* Setter for policy map used to create a classifier.
* @param policyMap a map of Throwable class to {@link BackOffPolicy} that will be
* used to create a {@link Classifier} to locate a policy.
*/
public void setPolicyMap(Map<Class<? extends Throwable>, BackOffPolicy> policyMap) {
throwableToBackOffPolicyClassifier = new SubclassClassifier<>(policyMap, new NoBackOffPolicy());
}

@Override
public BackOffContext start(RetryContext retryContext) {
// our backoff needs access to the last exception thrown so our backoff
// retryContext
// includes the retryContext (which has access to the last thrown exception)
return new BackOffByExceptionTypeContext(throwableToBackOffPolicyClassifier, retryContext);
}

@Override
public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
BackOffPolicy backOffPolicy = (BackOffPolicy) backOffContext;
backOffPolicy.backOff(backOffContext); // delegate to the context
}

static class BackOffByExceptionTypeContext implements BackOffContext, BackOffPolicy {

final protected Classifier<Throwable, BackOffPolicy> exceptionClassifier;

private RetryContext retryContext; // will have access to the last throwable

// we need a way to map from the exception to a backoff policy to a prior backoff
// context
private Map<BackOffPolicy, BackOffContext> backOffPolicyToBackOffContentMap = new HashMap<>();

BackOffByExceptionTypeContext(Classifier<Throwable, BackOffPolicy> exceptionClassifier,
RetryContext retryContext) {
this.exceptionClassifier = exceptionClassifier;
this.retryContext = retryContext;
}

@Override
public BackOffContext start(RetryContext context) {
// will never be called because ExceptionClassifierBackOffPolicy creates the
// context itself
return null;
}

@Override
public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
BackOffPolicy backOffPolicy = exceptionClassifier.classify(retryContext.getLastThrowable());
BackOffContext mappedBackoffPolicy = backOffPolicyToBackOffContentMap.get(backOffPolicy);
if (mappedBackoffPolicy == null) {
// we needed to postpone starting the backoff policy to here as the start
// api doesn't have access yet to the last throwable
mappedBackoffPolicy = backOffPolicy.start(retryContext);
backOffPolicyToBackOffContentMap.put(backOffPolicy, mappedBackoffPolicy);
}
backOffPolicy.backOff(mappedBackoffPolicy);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.springframework.retry.backoff;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

public class BackOffByExceptionTypePolicyTests {

@Test
public void defaultNoBackOff() {
BackOffByExceptionTypePolicy exceptionClassifierBackOffPolicy = new BackOffByExceptionTypePolicy();
BackOffByExceptionTypePolicy.BackOffByExceptionTypeContext startedBackOffContext = (BackOffByExceptionTypePolicy.BackOffByExceptionTypeContext) exceptionClassifierBackOffPolicy
.start(null);

BackOffPolicy backOffPolicy = startedBackOffContext.exceptionClassifier.classify(new IOException());
Assertions.assertTrue(backOffPolicy instanceof NoBackOffPolicy, "Expected NoBackOffPolicy ");
}

@Test
public void matchTheExceptionType() {
BackOffByExceptionTypePolicy exceptionClassifierBackOffPolicy = new BackOffByExceptionTypePolicy();
Map<Class<? extends Throwable>, BackOffPolicy> backOffPolicyMap = new HashMap<>();
backOffPolicyMap.put(IOException.class, new FixedBackOffPolicy());
backOffPolicyMap.put(SecurityException.class, new ExponentialBackOffPolicy());
exceptionClassifierBackOffPolicy.setPolicyMap(backOffPolicyMap);
BackOffByExceptionTypePolicy.BackOffByExceptionTypeContext startedBackOffContext = (BackOffByExceptionTypePolicy.BackOffByExceptionTypeContext) exceptionClassifierBackOffPolicy
.start(null);
BackOffPolicy backOffPolicy = startedBackOffContext.exceptionClassifier.classify(new IOException());
Assertions.assertTrue(backOffPolicy instanceof FixedBackOffPolicy, "Expected FixedBackOffPolicy ");
backOffPolicy = startedBackOffContext.exceptionClassifier.classify(new SecurityException());
Assertions.assertTrue(backOffPolicy instanceof ExponentialBackOffPolicy, "Expected FixedBackOffPolicy ");
}

@Test
public void testStatefulBackOff() throws IOException {
BackOffByExceptionTypePolicy exceptionClassifierBackOffPolicy = new BackOffByExceptionTypePolicy();
Map<Class<? extends Throwable>, BackOffPolicy> backOffPolicyMap = new HashMap<>();
ExponentialBackOffPolicy max222 = new ExponentialBackOffPolicy();
DummySleeper max222Sleeper = new DummySleeper();
max222.setSleeper(max222Sleeper);
max222.setMaxInterval(222);
ExponentialBackOffPolicy interval333 = new ExponentialBackOffPolicy();
DummySleeper initial333Sleeper = new DummySleeper();
interval333.setInitialInterval(333);
interval333.setSleeper(initial333Sleeper);
backOffPolicyMap.put(SecurityException.class, interval333);
backOffPolicyMap.put(IOException.class, max222);
exceptionClassifierBackOffPolicy.setPolicyMap(backOffPolicyMap);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(6));
retryTemplate.setBackOffPolicy(exceptionClassifierBackOffPolicy);
AtomicReference<Integer> count = new AtomicReference<>(0);
Integer execute = retryTemplate.execute(context -> {
count.getAndSet(count.get() + 1);
if (count.get() == 1) {
throw new SecurityException();
}
else if (count.get() < 5) {
throw new IOException();
}
else {
return count.get();
}
});

Assertions.assertArrayEquals(new long[] { 100, 200, 222 }, max222Sleeper.getBackOffs());
Assertions.assertArrayEquals(new long[] { 333 }, initial333Sleeper.getBackOffs());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@

package org.springframework.retry.backoff;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

Expand Down