From 9eebfe2288b19d15f01e6aad6c91724675236da0 Mon Sep 17 00:00:00 2001 From: Jon Travis Date: Mon, 28 May 2012 21:00:52 -0700 Subject: [PATCH] Added RetrySimulator - simulate retry + backoff loops - The RetrySimulator can be used to calibrate retry + backoff tuples. --- .../backoff/ExponentialBackOffPolicy.java | 20 ++- .../retry/backoff/FixedBackOffPolicy.java | 15 ++- .../retry/backoff/SleepingBackOffPolicy.java | 32 +++++ .../retry/policy/SimpleRetryPolicy.java | 5 + .../retry/support/RetrySimulation.java | 114 +++++++++++++++++ .../retry/support/RetrySimulator.java | 118 ++++++++++++++++++ .../retry/support/RetrySimulationTests.java | 96 ++++++++++++++ 7 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/springframework/retry/backoff/SleepingBackOffPolicy.java create mode 100644 src/main/java/org/springframework/retry/support/RetrySimulation.java create mode 100644 src/main/java/org/springframework/retry/support/RetrySimulator.java create mode 100644 src/test/java/org/springframework/retry/support/RetrySimulationTests.java diff --git a/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java index cdcbe49..7db23d8 100644 --- a/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java @@ -39,7 +39,7 @@ import org.springframework.util.ClassUtils; * @author Dave Syer * @author Gary Russell */ -public class ExponentialBackOffPolicy implements BackOffPolicy { +public class ExponentialBackOffPolicy implements SleepingBackOffPolicy { protected final Log logger = LogFactory.getLog(this.getClass()); /** @@ -84,6 +84,24 @@ public class ExponentialBackOffPolicy implements BackOffPolicy { this.sleeper = sleeper; } + public ExponentialBackOffPolicy withSleeper(Sleeper sleeper) { + ExponentialBackOffPolicy res = newInstance(); + cloneValues(res); + res.setSleeper(sleeper); + return res; + } + + protected ExponentialBackOffPolicy newInstance() { + return new ExponentialBackOffPolicy(); + } + + protected void cloneValues(ExponentialBackOffPolicy target) { + target.setInitialInterval(getInitialInterval()); + target.setMaxInterval(getMaxInterval()); + target.setMultiplier(getMultiplier()); + target.setSleeper(sleeper); + } + /** * Set the initial sleep interval value. Default is 100 * millisecond. Cannot be set to a value less than one. diff --git a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java index 665af8f..18e09f7 100644 --- a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java @@ -27,7 +27,7 @@ package org.springframework.retry.backoff; * @author Rob Harrop * @author Dave Syer */ -public class FixedBackOffPolicy extends StatelessBackOffPolicy { +public class FixedBackOffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy { /** * Default back off period - 1000ms. @@ -41,7 +41,14 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy { private Sleeper sleeper = new ObjectWaitSleeper(); - + + public FixedBackOffPolicy withSleeper(Sleeper sleeper) { + FixedBackOffPolicy res = new FixedBackOffPolicy(); + res.setBackOffPeriod(backOffPeriod); + res.setSleeper(sleeper); + return res; + } + /** * Public setter for the {@link Sleeper} strategy. * @param sleeper the sleeper to set defaults to {@link ObjectWaitSleeper}. @@ -78,4 +85,8 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy { throw new BackOffInterruptedException("Thread interrupted while sleeping", e); } } + + public String toString() { + return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]"; + } } diff --git a/src/main/java/org/springframework/retry/backoff/SleepingBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/SleepingBackOffPolicy.java new file mode 100644 index 0000000..96d2ea6 --- /dev/null +++ b/src/main/java/org/springframework/retry/backoff/SleepingBackOffPolicy.java @@ -0,0 +1,32 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.backoff; + +/** + * A interface which can be mixed in by {@link BackOffPolicy}s indicating that they sleep + * when backing off. + */ +public interface SleepingBackOffPolicy extends BackOffPolicy { + /** + * Clone the policy and return a new policy which uses the passed sleeper. + * + * @param sleeper Target to be invoked any time the backoff policy sleeps + * @return a clone of this policy which will have all of its backoff sleeps + * routed into the passed sleeper + */ + T withSleeper(Sleeper sleeper); +} diff --git a/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java index 52ef10d..ed2f727 100644 --- a/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java @@ -23,6 +23,7 @@ import org.springframework.classify.BinaryExceptionClassifier; import org.springframework.retry.RetryContext; import org.springframework.retry.RetryPolicy; import org.springframework.retry.context.RetryContextSupport; +import org.springframework.util.ClassUtils; /** * @@ -150,4 +151,8 @@ public class SimpleRetryPolicy implements RetryPolicy { private boolean retryForException(Throwable ex) { return retryableClassifier.classify(ex); } + + public String toString() { + return ClassUtils.getShortName(getClass()) + "[maxAttempts=" + maxAttempts + "]"; + } } diff --git a/src/main/java/org/springframework/retry/support/RetrySimulation.java b/src/main/java/org/springframework/retry/support/RetrySimulation.java new file mode 100644 index 0000000..1475646 --- /dev/null +++ b/src/main/java/org/springframework/retry/support/RetrySimulation.java @@ -0,0 +1,114 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.support; + + +import java.util.*; + +/** + * The results of a simulation. + */ +public class RetrySimulation { + private final List sleepSequences = new ArrayList(); + private final Map sleepHistogram = new HashMap(); + + /** + * Add a sequence of sleeps to the simulation. + */ + public void addSequence(List sleeps) { + for (Long sleep : sleeps) { + Long existingHisto = sleepHistogram.get(sleep); + if (existingHisto == null) { + sleepHistogram.put(sleep, 1l); + } else { + sleepHistogram.put(sleep, existingHisto + 1); + } + } + + sleepSequences.add(new SleepSequence(sleeps)); + } + + /** + * @return Returns a list of all the unique sleep values which were executed within + * all simulations. + */ + public List getUniqueSleeps() { + List res = new ArrayList(sleepHistogram.keySet()); + Collections.sort(res); + return res; + } + + /** + * @return the count of each sleep which was seen throughout all sleeps. + * histogram[i] = sum(getUniqueSleeps()[i]) + */ + public List getUniqueSleepsHistogram() { + List res = new ArrayList(sleepHistogram.size()); + for (Long sleep : getUniqueSleeps()) { + res.add(sleepHistogram.get(sleep)); + } + return res; + } + + /** + * @return the longest total time slept by a retry sequence. + */ + public SleepSequence getLongestTotalSleepSequence() { + SleepSequence longest = null; + for (SleepSequence sequence : sleepSequences) { + if (longest == null || sequence.getTotalSleep() > longest.getTotalSleep()) { + longest = sequence; + } + } + return longest; + } + + public static class SleepSequence { + private final List sleeps; + private final long longestSleep; + private final long totalSleep; + + public SleepSequence(List sleeps) { + this.sleeps = sleeps; + this.longestSleep = Collections.max(sleeps); + long totalSleep = 0; + for (Long sleep : sleeps) { + totalSleep += sleep; + } + this.totalSleep = totalSleep; + } + + public List getSleeps() { + return sleeps; + } + + /** + * Returns the longest individual sleep within this sequence. + */ + public long getLongestSleep() { + return longestSleep; + } + + public long getTotalSleep() { + return totalSleep; + } + + public String toString() { + return "totalSleep=" + totalSleep + ": " + sleeps.toString(); + } + } +} diff --git a/src/main/java/org/springframework/retry/support/RetrySimulator.java b/src/main/java/org/springframework/retry/support/RetrySimulator.java new file mode 100644 index 0000000..39e7e0e --- /dev/null +++ b/src/main/java/org/springframework/retry/support/RetrySimulator.java @@ -0,0 +1,118 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.support; + +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.backoff.Sleeper; +import org.springframework.retry.backoff.SleepingBackOffPolicy; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link RetrySimulator} is a tool for exercising retry + backoff operations. + * + * When calibrating a set of retry + backoff pairs, it is useful to know the behaviour + * of the retry for various scenarios. + * + * Things you may want to know: + * - Does a 'maxInterval' of 5000 ms in my backoff even matter? + * (This is often the case when retry counts are low -- so why set the max interval + * at something that cannot be achieved?) + * - What are the typical sleep durations for threads in a retry + * - What was the longest sleep duration for any retry sequence + * + * The simulator provides this information by executing a retry + backoff pair until failure + * (that is all retries are exhausted). The information about each retry is provided + * as part of the {@link RetrySimulation}. + * + * Note that the impetus for this class was to expose the timings which are possible with + * {@link org.springframework.retry.backoff.ExponentialRandomBackOffPolicy}, which provides + * random values and must be looked at over a series of trials. + * + * @author Jon Travis + */ +public class RetrySimulator { + private final SleepingBackOffPolicy backOffPolicy; + private final RetryPolicy retryPolicy; + + public RetrySimulator(SleepingBackOffPolicy backOffPolicy, RetryPolicy retryPolicy) { + this.backOffPolicy = backOffPolicy; + this.retryPolicy = retryPolicy; + } + + /** + * Execute the simulator for a give # of iterations. + * + * @param numSimulations Number of simulations to run + * @return the outcome of all simulations + */ + public RetrySimulation executeSimulation(int numSimulations) { + RetrySimulation simulation = new RetrySimulation(); + + for (int i=0; i executeSingleSimulation() { + StealingSleeper stealingSleeper = new StealingSleeper(); + SleepingBackOffPolicy stealingBackoff = backOffPolicy.withSleeper(stealingSleeper); + + RetryTemplate template = new RetryTemplate(); + template.setBackOffPolicy(stealingBackoff); + template.setRetryPolicy(retryPolicy); + + try { + template.execute(new FailingRetryCallback()); + } catch(FailingRetryException e) { + + } catch(Exception e) { + throw new RuntimeException("Unexpected exception", e); + } + + return stealingSleeper.getSleeps(); + } + + static class FailingRetryCallback implements RetryCallback { + public Object doWithRetry(RetryContext context) throws Exception { + throw new FailingRetryException(); + } + } + + static class FailingRetryException extends Exception { + } + + static class StealingSleeper implements Sleeper { + private final List sleeps = new ArrayList(); + + public void sleep(long backOffPeriod) throws InterruptedException { + sleeps.add(backOffPeriod); + } + + public List getSleeps() { + return sleeps; + } + } +} diff --git a/src/test/java/org/springframework/retry/support/RetrySimulationTests.java b/src/test/java/org/springframework/retry/support/RetrySimulationTests.java new file mode 100644 index 0000000..f0ff6fc --- /dev/null +++ b/src/test/java/org/springframework/retry/support/RetrySimulationTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.support; + +import org.junit.Test; +import org.springframework.retry.backoff.ExponentialBackOffPolicy; +import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RetrySimulationTests { + @Test + public void testSimulatorExercisesFixedBackoff() { + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(5); + + FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); + backOffPolicy.setBackOffPeriod(400); + + RetrySimulator simulator = new RetrySimulator(backOffPolicy, retryPolicy); + RetrySimulation simulation = simulator.executeSimulation(1000); + System.out.println(backOffPolicy); + System.out.println("Longest sequence " + simulation.getLongestTotalSleepSequence()); + System.out.println("All Sleeps: " + simulation.getUniqueSleeps()); + System.out.println("Sleep Occurences: " + simulation.getUniqueSleepsHistogram()); + + assertEquals(asList(400l, 400l, 400l, 400l), simulation.getLongestTotalSleepSequence().getSleeps()); + assertEquals(asList(400l), simulation.getUniqueSleeps()); + assertEquals(asList(4000l), simulation.getUniqueSleepsHistogram()); + } + + @Test + public void testSimulatorExercisesExponentialBackoff() { + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(5); + + ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); + backOffPolicy.setMultiplier(2); + backOffPolicy.setMaxInterval(30000); + backOffPolicy.setInitialInterval(100); + + RetrySimulator simulator = new RetrySimulator(backOffPolicy, retryPolicy); + RetrySimulation simulation = simulator.executeSimulation(1000); + System.out.println(backOffPolicy); + System.out.println("Longest sequence " + simulation.getLongestTotalSleepSequence()); + System.out.println("All Sleeps: " + simulation.getUniqueSleeps()); + System.out.println("Sleep Occurences: " + simulation.getUniqueSleepsHistogram()); + + assertEquals(asList(100l, 200l, 400l, 800l), simulation.getLongestTotalSleepSequence().getSleeps()); + assertEquals(asList(100l, 200l, 400l, 800l), simulation.getUniqueSleeps()); + assertEquals(asList(1000l, 1000l, 1000l, 1000l), simulation.getUniqueSleepsHistogram()); + } + + @Test + public void testSimulatorExercisesRandomExponentialBackoff() { + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(5); + + ExponentialBackOffPolicy backOffPolicy = new ExponentialRandomBackOffPolicy(); + backOffPolicy.setMultiplier(2); + backOffPolicy.setMaxInterval(30000); + backOffPolicy.setInitialInterval(100); + + RetrySimulator simulator = new RetrySimulator(backOffPolicy, retryPolicy); + RetrySimulation simulation = simulator.executeSimulation(10000); + System.out.println(backOffPolicy); + System.out.println("Longest sequence " + simulation.getLongestTotalSleepSequence()); + System.out.println("All Sleeps: " + simulation.getUniqueSleeps()); + System.out.println("Sleep Occurences: " + simulation.getUniqueSleepsHistogram()); + + assertEquals(asList(100l, 200l, 400l, 800l), simulation.getLongestTotalSleepSequence().getSleeps()); + assertEquals(asList(100l, 200l, 300l, 400l, 500l, 600l, 700l, 800l), simulation.getUniqueSleeps()); + for (long histo : simulation.getUniqueSleepsHistogram()) { + assertTrue("Should have experienced a sleep more than " + histo + " times", + histo > 1000); + } + } +}