ExponentialBackoffRetryLogic implements RetryLogic { @Override public <T> T retry( Supplier<T> work ) { List<Throwable> errors = null; long startTime = -1; long nextDelayMs = initialRetryDelayMs; while ( true ) { try { return work.get(); } catch ( Throwable throwable ) { Throwable error = extractPossibleTerminationCause( throwable ); if ( canRetryOn( error ) ) { long currentTime = clock.millis(); if ( startTime == -1 ) { startTime = currentTime; } long elapsedTime = currentTime - startTime; if ( elapsedTime < maxRetryTimeMs ) { long delayWithJitterMs = computeDelayWithJitter( nextDelayMs ); log.warn( "Transaction failed and will be retried in " + delayWithJitterMs + "ms", error ); sleep( delayWithJitterMs ); nextDelayMs = (long) (nextDelayMs * multiplier); errors = recordError( error, errors ); continue; } } addSuppressed( throwable, errors ); throw throwable; } } } ExponentialBackoffRetryLogic( RetrySettings settings, EventExecutorGroup eventExecutorGroup, Clock clock,
            Logging logging );   ExponentialBackoffRetryLogic( long maxRetryTimeMs, long initialRetryDelayMs, double multiplier,
            double jitterFactor, EventExecutorGroup eventExecutorGroup, Clock clock, Logging logging ); @Override T retry( Supplier<T> work ); @Override CompletionStage<T> retryAsync( Supplier<CompletionStage<T>> work ); @Override Publisher<T> retryRx( Publisher<T> work ); @Experimental static boolean isRetryable( Throwable error );  }
@Test void nextDelayCalculatedAccordingToMultiplier() throws Exception { int retries = 27; int initialDelay = 1; int multiplier = 3; int noJitter = 0; Clock clock = mock( Clock.class ); ExponentialBackoffRetryLogic logic = newRetryLogic( MAX_VALUE, initialDelay, multiplier, noJitter, clock ); retry( logic, retries ); assertEquals( delaysWithoutJitter( initialDelay, multiplier, retries ), sleepValues( clock, retries ) ); }
@Test void nextDelayCalculatedAccordingToJitter() throws Exception { int retries = 32; double jitterFactor = 0.2; int initialDelay = 1; int multiplier = 2; Clock clock = mock( Clock.class ); ExponentialBackoffRetryLogic logic = newRetryLogic( MAX_VALUE, initialDelay, multiplier, jitterFactor, clock ); retry( logic, retries ); List<Long> sleepValues = sleepValues( clock, retries ); List<Long> delaysWithoutJitter = delaysWithoutJitter( initialDelay, multiplier, retries ); assertDelaysApproximatelyEqual( delaysWithoutJitter, sleepValues, jitterFactor ); }
@Test void doesRetryOnClientExceptionWithRetryableCause() { Clock clock = mock( Clock.class ); Logging logging = mock( Logging.class ); Logger logger = mock( Logger.class ); when( logging.getLog( anyString() ) ).thenReturn( logger ); ExponentialBackoffRetryLogic logic = new ExponentialBackoffRetryLogic( RetrySettings.DEFAULT, eventExecutor, clock, logging ); AtomicBoolean exceptionThrown = new AtomicBoolean( false ); String result = logic.retry( () -> { if ( exceptionThrown.compareAndSet( false, true ) ) { throw clientExceptionWithValidTerminationCause(); } return "Done"; } ); assertEquals( "Done", result ); }
@Test void doesNotRetryOnRandomClientException() { Clock clock = mock( Clock.class ); Logging logging = mock( Logging.class ); Logger logger = mock( Logger.class ); when( logging.getLog( anyString() ) ).thenReturn( logger ); ExponentialBackoffRetryLogic logic = new ExponentialBackoffRetryLogic( RetrySettings.DEFAULT, eventExecutor, clock, logging ); AtomicBoolean exceptionThrown = new AtomicBoolean( false ); ClientException exception = Assertions.assertThrows( ClientException.class, () -> logic.retry( () -> { if ( exceptionThrown.compareAndSet( false, true ) ) { throw randomClientException(); } return "Done"; } ) ); assertEquals( "Meeh", exception.getMessage() ); }
@Test void eachRetryIsLogged() { int retries = 9; Clock clock = mock( Clock.class ); Logging logging = mock( Logging.class ); Logger logger = mock( Logger.class ); when( logging.getLog( anyString() ) ).thenReturn( logger ); ExponentialBackoffRetryLogic logic = new ExponentialBackoffRetryLogic( RetrySettings.DEFAULT, eventExecutor, clock, logging ); retry( logic, retries ); verify( logger, times( retries ) ).warn( startsWith( "Transaction failed and will be retried" ), any( ServiceUnavailableException.class ) ); }
@Test void nothingIsLoggedOnFatalFailure() { Logging logging = mock( Logging.class ); Logger logger = mock( Logger.class ); when( logging.getLog( anyString() ) ).thenReturn( logger ); ExponentialBackoffRetryLogic logic = new ExponentialBackoffRetryLogic( RetrySettings.DEFAULT, eventExecutor, mock( Clock.class ), logging ); RuntimeException error = assertThrows( RuntimeException.class, () -> logic.retry( () -> { throw new RuntimeException( "Fatal blocking" ); } ) ); assertEquals( "Fatal blocking", error.getMessage() ); verifyZeroInteractions( logger ); }
@Test void correctNumberOfRetiesAreLoggedOnFailure() { Clock clock = mock( Clock.class ); Logging logging = mock( Logging.class ); Logger logger = mock( Logger.class ); when( logging.getLog( anyString() ) ).thenReturn( logger ); RetrySettings settings = RetrySettings.DEFAULT; RetryLogic logic = new ExponentialBackoffRetryLogic( settings, eventExecutor, clock, logging ); ServiceUnavailableException error = assertThrows( ServiceUnavailableException.class, () -> logic.retry( new Supplier<Long>() { boolean invoked; @Override public Long get() { if ( invoked ) { when( clock.millis() ).thenReturn( settings.maxRetryTimeMs() + 42 ); } else { invoked = true; } throw new ServiceUnavailableException( "Error" ); } } ) ); assertEquals( "Error", error.getMessage() ); verify( logger ).warn( startsWith( "Transaction failed and will be retried" ), any( ServiceUnavailableException.class ) ); }