From 699e8e8ed133d744d089dd4f5b5871a033a0942d Mon Sep 17 00:00:00 2001 From: TONYC Date: Mon, 13 Nov 2023 18:08:06 -0500 Subject: [PATCH] Add BackOffByExceptionTypePolicy to backoff by exception type --- .../backoff/BackOffByExceptionTypePolicy.java | 85 +++++++++++++++++++ .../BackOffByExceptionTypePolicyTests.java | 78 +++++++++++++++++ .../BackOffPolicySerializationTests.java | 5 +- 3 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicy.java create mode 100644 src/test/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicyTests.java diff --git a/src/main/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicy.java b/src/main/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicy.java new file mode 100644 index 00000000..f6db3fda --- /dev/null +++ b/src/main/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicy.java @@ -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 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, 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 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 backOffPolicyToBackOffContentMap = new HashMap<>(); + + BackOffByExceptionTypeContext(Classifier 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); + } + + } + +} diff --git a/src/test/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicyTests.java b/src/test/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicyTests.java new file mode 100644 index 00000000..109f8289 --- /dev/null +++ b/src/test/java/org/springframework/retry/backoff/BackOffByExceptionTypePolicyTests.java @@ -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, 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, 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 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()); + } + +} diff --git a/src/test/java/org/springframework/retry/backoff/BackOffPolicySerializationTests.java b/src/test/java/org/springframework/retry/backoff/BackOffPolicySerializationTests.java index d7440d6a..fa91d3ea 100644 --- a/src/test/java/org/springframework/retry/backoff/BackOffPolicySerializationTests.java +++ b/src/test/java/org/springframework/retry/backoff/BackOffPolicySerializationTests.java @@ -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;