/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* 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.neo4j.driver.internal.retry;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.List;
import org.neo4j.driver.internal.logging.DevNullLogging;
import org.neo4j.driver.internal.util.Clock;
import org.neo4j.driver.internal.util.Supplier;
import org.neo4j.driver.v1.Logger;
import org.neo4j.driver.v1.Logging;
import org.neo4j.driver.v1.exceptions.ServiceUnavailableException;
import org.neo4j.driver.v1.exceptions.SessionExpiredException;
import org.neo4j.driver.v1.exceptions.TransientException;
import static java.lang.Long.MAX_VALUE;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.startsWith;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ExponentialBackoffRetryLogicTest
{
@Test
public void throwsForIllegalMaxRetryTime()
{
try
{
newRetryLogic( -100, 1, 1, 1, Clock.SYSTEM );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalArgumentException.class ) );
assertThat( e.getMessage(), containsString( "Max retry time" ) );
}
}
@Test
public void throwsForIllegalInitialRetryDelay()
{
try
{
newRetryLogic( 1, -100, 1, 1, Clock.SYSTEM );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalArgumentException.class ) );
assertThat( e.getMessage(), containsString( "Initial retry delay" ) );
}
}
@Test
public void throwsForIllegalMultiplier()
{
try
{
newRetryLogic( 1, 1, 0.42, 1, Clock.SYSTEM );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalArgumentException.class ) );
assertThat( e.getMessage(), containsString( "Multiplier" ) );
}
}
@Test
public void throwsForIllegalJitterFactor()
{
try
{
newRetryLogic( 1, 1, 1, -0.42, Clock.SYSTEM );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalArgumentException.class ) );
assertThat( e.getMessage(), containsString( "Jitter" ) );
}
try
{
newRetryLogic( 1, 1, 1, 1.42, Clock.SYSTEM );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalArgumentException.class ) );
assertThat( e.getMessage(), containsString( "Jitter" ) );
}
}
@Test
public void throwsForIllegalClock()
{
try
{
newRetryLogic( 1, 1, 1, 1, null );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalArgumentException.class ) );
assertThat( e.getMessage(), containsString( "Clock" ) );
}
}
@Test
public 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
public 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 );
assertEquals( delaysWithoutJitter.size(), sleepValues.size() );
for ( int i = 0; i < sleepValues.size(); i++ )
{
double sleepValue = sleepValues.get( i ).doubleValue();
long delayWithoutJitter = delaysWithoutJitter.get( i );
double jitter = delayWithoutJitter * jitterFactor;
assertThat( sleepValue, closeTo( delayWithoutJitter, jitter ) );
}
}
@Test
public void doesNotRetryWhenMaxRetryTimeExceeded() throws Exception
{
long retryStart = Clock.SYSTEM.millis();
int initialDelay = 100;
int multiplier = 2;
long maxRetryTimeMs = 45;
Clock clock = mock( Clock.class );
when( clock.millis() ).thenReturn( retryStart )
.thenReturn( retryStart + maxRetryTimeMs - 5 )
.thenReturn( retryStart + maxRetryTimeMs + 7 );
ExponentialBackoffRetryLogic logic = newRetryLogic( maxRetryTimeMs, initialDelay, multiplier, 0, clock );
Supplier<Void> workMock = newWorkMock();
SessionExpiredException error = sessionExpired();
when( workMock.get() ).thenThrow( error );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertEquals( error, e );
}
verify( clock ).sleep( initialDelay );
verify( clock ).sleep( initialDelay * multiplier );
verify( workMock, times( 3 ) ).get();
}
@Test
public void sleepsOnServiceUnavailableException() throws Exception
{
Clock clock = mock( Clock.class );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 42, 1, 0, clock );
Supplier<Void> workMock = newWorkMock();
ServiceUnavailableException error = serviceUnavailable();
when( workMock.get() ).thenThrow( error ).thenReturn( null );
assertNull( logic.retry( workMock ) );
verify( workMock, times( 2 ) ).get();
verify( clock ).sleep( 42 );
}
@Test
public void sleepsOnSessionExpiredException() throws Exception
{
Clock clock = mock( Clock.class );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 4242, 1, 0, clock );
Supplier<Void> workMock = newWorkMock();
SessionExpiredException error = sessionExpired();
when( workMock.get() ).thenThrow( error ).thenReturn( null );
assertNull( logic.retry( workMock ) );
verify( workMock, times( 2 ) ).get();
verify( clock ).sleep( 4242 );
}
@Test
public void sleepsOnTransientException() throws Exception
{
Clock clock = mock( Clock.class );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 23, 1, 0, clock );
Supplier<Void> workMock = newWorkMock();
TransientException error = transientException();
when( workMock.get() ).thenThrow( error ).thenReturn( null );
assertNull( logic.retry( workMock ) );
verify( workMock, times( 2 ) ).get();
verify( clock ).sleep( 23 );
}
@Test
public void throwsWhenUnknownError() throws Exception
{
Clock clock = mock( Clock.class );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 1, 1, 1, clock );
Supplier<Void> workMock = newWorkMock();
IllegalStateException error = new IllegalStateException();
when( workMock.get() ).thenThrow( error );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertEquals( error, e );
}
verify( workMock ).get();
verify( clock, never() ).sleep( anyLong() );
}
@Test
public void throwsWhenTransactionTerminatedError() throws Exception
{
Clock clock = mock( Clock.class );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 13, 1, 0, clock );
Supplier<Void> workMock = newWorkMock();
TransientException error = new TransientException( "Neo.TransientError.Transaction.Terminated", "" );
when( workMock.get() ).thenThrow( error ).thenReturn( null );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertEquals( error, e );
}
verify( workMock ).get();
verify( clock, never() ).sleep( 13 );
}
@Test
public void throwsWhenTransactionLockClientStoppedError() throws Exception
{
Clock clock = mock( Clock.class );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 13, 1, 0, clock );
Supplier<Void> workMock = newWorkMock();
TransientException error = new TransientException( "Neo.TransientError.Transaction.LockClientStopped", "" );
when( workMock.get() ).thenThrow( error ).thenReturn( null );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertEquals( error, e );
}
verify( workMock ).get();
verify( clock, never() ).sleep( 13 );
}
@Test
public void throwsWhenSleepInterrupted() throws Exception
{
Clock clock = mock( Clock.class );
doThrow( new InterruptedException() ).when( clock ).sleep( 1 );
ExponentialBackoffRetryLogic logic = newRetryLogic( 1, 1, 1, 0, clock );
Supplier<Void> workMock = newWorkMock();
when( workMock.get() ).thenThrow( serviceUnavailable() );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( IllegalStateException.class ) );
assertThat( e.getCause(), instanceOf( InterruptedException.class ) );
}
finally
{
// Clear the interruption flag so all subsequent tests do not see this thread as interrupted
Thread.interrupted();
}
}
@Test
public void collectsSuppressedErrors() throws Exception
{
long maxRetryTime = 20;
int initialDelay = 15;
int multiplier = 2;
Clock clock = mock( Clock.class );
when( clock.millis() ).thenReturn( 0L ).thenReturn( 10L ).thenReturn( 15L ).thenReturn( 25L );
ExponentialBackoffRetryLogic logic = newRetryLogic( maxRetryTime, initialDelay, multiplier, 0, clock );
Supplier<Void> workMock = newWorkMock();
SessionExpiredException error1 = sessionExpired();
SessionExpiredException error2 = sessionExpired();
ServiceUnavailableException error3 = serviceUnavailable();
TransientException error4 = transientException();
when( workMock.get() ).thenThrow( error1, error2, error3, error4 ).thenReturn( null );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertEquals( error4, e );
Throwable[] suppressed = e.getSuppressed();
assertEquals( 3, suppressed.length );
assertEquals( error1, suppressed[0] );
assertEquals( error2, suppressed[1] );
assertEquals( error3, suppressed[2] );
}
verify( workMock, times( 4 ) ).get();
verify( clock, times( 3 ) ).sleep( anyLong() );
verify( clock ).sleep( initialDelay );
verify( clock ).sleep( initialDelay * multiplier );
verify( clock ).sleep( initialDelay * multiplier * multiplier );
}
@Test
public void doesNotCollectSuppressedErrorsWhenSameErrorIsThrown() throws Exception
{
long maxRetryTime = 20;
int initialDelay = 15;
int multiplier = 2;
Clock clock = mock( Clock.class );
when( clock.millis() ).thenReturn( 0L ).thenReturn( 10L ).thenReturn( 25L );
ExponentialBackoffRetryLogic logic = newRetryLogic( maxRetryTime, initialDelay, multiplier, 0, clock );
Supplier<Void> workMock = newWorkMock();
SessionExpiredException error = sessionExpired();
when( workMock.get() ).thenThrow( error );
try
{
logic.retry( workMock );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertEquals( error, e );
assertEquals( 0, e.getSuppressed().length );
}
verify( workMock, times( 3 ) ).get();
verify( clock, times( 2 ) ).sleep( anyLong() );
verify( clock ).sleep( initialDelay );
verify( clock ).sleep( initialDelay * multiplier );
}
@Test
public 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, clock, logging );
retry( logic, retries );
verify( logger, times( retries ) ).error(
startsWith( "Transaction failed and will be retried" ),
any( ServiceUnavailableException.class )
);
}
private static void retry( ExponentialBackoffRetryLogic retryLogic, final int times )
{
retryLogic.retry( new Supplier<Void>()
{
int invoked;
@Override
public Void get()
{
if ( invoked < times )
{
invoked++;
throw serviceUnavailable();
}
return null;
}
} );
}
private static List<Long> delaysWithoutJitter( long initialDelay, double multiplier, int count )
{
List<Long> values = new ArrayList<>();
long delay = initialDelay;
do
{
values.add( delay );
delay *= multiplier;
}
while ( --count > 0 );
return values;
}
private static List<Long> sleepValues( Clock clockMock, int expectedCount ) throws InterruptedException
{
ArgumentCaptor<Long> captor = ArgumentCaptor.forClass( long.class );
verify( clockMock, times( expectedCount ) ).sleep( captor.capture() );
return captor.getAllValues();
}
private static ExponentialBackoffRetryLogic newRetryLogic( long maxRetryTimeMs, long initialRetryDelayMs,
double multiplier, double jitterFactor, Clock clock )
{
return new ExponentialBackoffRetryLogic( maxRetryTimeMs, initialRetryDelayMs, multiplier, jitterFactor, clock,
DevNullLogging.DEV_NULL_LOGGING );
}
private static ServiceUnavailableException serviceUnavailable()
{
return new ServiceUnavailableException( "" );
}
private static SessionExpiredException sessionExpired()
{
return new SessionExpiredException( "" );
}
private static TransientException transientException()
{
return new TransientException( "", "" );
}
@SuppressWarnings( "unchecked" )
private static Supplier<Void> newWorkMock()
{
return mock( Supplier.class );
}
}