/* * 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.net.pooling; import org.junit.Test; import org.mockito.InOrder; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.Connector; import org.neo4j.driver.internal.spi.PooledConnection; import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.internal.util.FakeClock; import org.neo4j.driver.v1.Logging; import static java.util.Collections.newSetFromMap; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.RETURNS_MOCKS; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; 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; import static org.neo4j.driver.internal.net.BoltServerAddress.DEFAULT_PORT; import static org.neo4j.driver.internal.net.BoltServerAddress.LOCAL_DEFAULT; public class SocketConnectionPoolTest { private static final BoltServerAddress ADDRESS_1 = LOCAL_DEFAULT; private static final BoltServerAddress ADDRESS_2 = new BoltServerAddress( "localhost", DEFAULT_PORT + 42 ); private static final BoltServerAddress ADDRESS_3 = new BoltServerAddress( "localhost", DEFAULT_PORT + 4242 ); @Test public void acquireCreatesNewConnectionWhenPoolIsEmpty() { Connector connector = newMockConnector(); SocketConnectionPool pool = newPool( connector ); Connection connection = pool.acquire( ADDRESS_1 ); assertThat( connection, instanceOf( PooledConnection.class ) ); verify( connector ).connect( ADDRESS_1 ); } @Test public void acquireUsesExistingConnectionIfPresent() { Connection connection = newConnectionMock( ADDRESS_1 ); Connector connector = newMockConnector( connection ); SocketConnectionPool pool = newPool( connector ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); assertThat( acquiredConnection1, instanceOf( PooledConnection.class ) ); acquiredConnection1.close(); // return connection to the pool Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); assertThat( acquiredConnection2, instanceOf( PooledConnection.class ) ); verify( connector ).connect( ADDRESS_1 ); } @Test public void purgeDoesNothingForNonExistingAddress() { Connection connection = newConnectionMock( ADDRESS_1 ); SocketConnectionPool pool = newPool( newMockConnector( connection ) ); pool.acquire( ADDRESS_1 ).close(); assertTrue( pool.hasAddress( ADDRESS_1 ) ); pool.purge( ADDRESS_2 ); assertTrue( pool.hasAddress( ADDRESS_1 ) ); } @Test public void purgeRemovesAddress() { Connection connection = newConnectionMock( ADDRESS_1 ); SocketConnectionPool pool = newPool( newMockConnector( connection ) ); pool.acquire( ADDRESS_1 ).close(); assertTrue( pool.hasAddress( ADDRESS_1 ) ); pool.purge( ADDRESS_1 ); assertFalse( pool.hasAddress( ADDRESS_1 ) ); } @Test public void purgeTerminatesPoolCorrespondingToTheAddress() { Connection connection1 = newConnectionMock( ADDRESS_1 ); Connection connection2 = newConnectionMock( ADDRESS_1 ); Connection connection3 = newConnectionMock( ADDRESS_1 ); SocketConnectionPool pool = newPool( newMockConnector( connection1, connection2, connection3 ) ); Connection pooledConnection1 = pool.acquire( ADDRESS_1 ); Connection pooledConnection2 = pool.acquire( ADDRESS_1 ); pool.acquire( ADDRESS_1 ); // return two connections to the pool pooledConnection1.close(); pooledConnection2.close(); pool.purge( ADDRESS_1 ); verify( connection1 ).close(); verify( connection2 ).close(); verify( connection3 ).close(); } @Test public void hasAddressReturnsFalseWhenPoolIsEmpty() { SocketConnectionPool pool = newPool( newMockConnector() ); assertFalse( pool.hasAddress( ADDRESS_1 ) ); assertFalse( pool.hasAddress( ADDRESS_2 ) ); } @Test public void hasAddressReturnsFalseForUnknownAddress() { SocketConnectionPool pool = newPool( newMockConnector() ); assertNotNull( pool.acquire( ADDRESS_1 ) ); assertFalse( pool.hasAddress( ADDRESS_2 ) ); } @Test public void hasAddressReturnsTrueForKnownAddress() { SocketConnectionPool pool = newPool( newMockConnector() ); assertNotNull( pool.acquire( ADDRESS_1 ) ); assertTrue( pool.hasAddress( ADDRESS_1 ) ); } @Test public void closeTerminatesAllPools() { Connection connection1 = newConnectionMock( ADDRESS_1 ); Connection connection2 = newConnectionMock( ADDRESS_1 ); Connection connection3 = newConnectionMock( ADDRESS_2 ); Connection connection4 = newConnectionMock( ADDRESS_2 ); Connector connector = newMockConnector( connection1, connection2, connection3, connection4 ); SocketConnectionPool pool = newPool( connector ); assertNotNull( pool.acquire( ADDRESS_1 ) ); pool.acquire( ADDRESS_1 ).close(); // return to the pool assertNotNull( pool.acquire( ADDRESS_2 ) ); pool.acquire( ADDRESS_2 ).close(); // return to the pool assertTrue( pool.hasAddress( ADDRESS_1 ) ); assertTrue( pool.hasAddress( ADDRESS_2 ) ); pool.close(); verify( connection1 ).close(); verify( connection2 ).close(); verify( connection3 ).close(); verify( connection4 ).close(); } @Test public void closeRemovesAllPools() { Connection connection1 = newConnectionMock( ADDRESS_1 ); Connection connection2 = newConnectionMock( ADDRESS_2 ); Connection connection3 = newConnectionMock( ADDRESS_3 ); Connector connector = newMockConnector( connection1, connection2, connection3 ); SocketConnectionPool pool = newPool( connector ); assertNotNull( pool.acquire( ADDRESS_1 ) ); assertNotNull( pool.acquire( ADDRESS_2 ) ); assertNotNull( pool.acquire( ADDRESS_3 ) ); assertTrue( pool.hasAddress( ADDRESS_1 ) ); assertTrue( pool.hasAddress( ADDRESS_2 ) ); assertTrue( pool.hasAddress( ADDRESS_3 ) ); pool.close(); assertFalse( pool.hasAddress( ADDRESS_1 ) ); assertFalse( pool.hasAddress( ADDRESS_2 ) ); assertFalse( pool.hasAddress( ADDRESS_3 ) ); } @Test public void closeWithConcurrentAcquisitionsEmptiesThePool() throws InterruptedException { Connector connector = mock( Connector.class ); Set<Connection> createdConnections = newSetFromMap( new ConcurrentHashMap<Connection,Boolean>() ); when( connector.connect( any( BoltServerAddress.class ) ) ) .then( createConnectionAnswer( createdConnections ) ); SocketConnectionPool pool = newPool( connector ); ExecutorService executor = Executors.newCachedThreadPool(); List<Future<Void>> results = new ArrayList<>(); AtomicInteger port = new AtomicInteger(); for ( int i = 0; i < 5; i++ ) { Future<Void> result = executor.submit( acquireConnection( pool, port ) ); results.add( result ); } Thread.sleep( 500 ); // allow workers to do something pool.close(); for ( Future<Void> result : results ) { try { result.get( 20, TimeUnit.SECONDS ); fail( "Exception expected" ); } catch ( Exception e ) { assertThat( e, instanceOf( ExecutionException.class ) ); assertThat( e.getCause(), instanceOf( IllegalStateException.class ) ); } } executor.shutdownNow(); executor.awaitTermination( 10, TimeUnit.SECONDS ); for ( int i = 0; i < port.intValue(); i++ ) { boolean hasAddress = pool.hasAddress( new BoltServerAddress( "localhost", i ) ); assertFalse( "Pool still has connection queues" + pool, hasAddress ); } for ( Connection connection : createdConnections ) { verify( connection ).close(); } } @Test public void recentlyUsedConnectionNotValidatedDuringAcquisition() { long idleTimeBeforeConnectionTest = 100; long creationTimestamp = 42; long closedAfterMs = 10; long acquiredAfterMs = 20; Connection connection = newConnectionMock( ADDRESS_1 ); FakeClock clock = new FakeClock(); SocketConnectionPool pool = newPool( newMockConnector( connection ), clock, idleTimeBeforeConnectionTest ); clock.progress( creationTimestamp ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); verify( connection, never() ).reset(); verify( connection, never() ).sync(); // return to the pool clock.progress( closedAfterMs ); acquiredConnection1.close(); verify( connection ).reset(); verify( connection ).sync(); clock.progress( acquiredAfterMs ); Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); assertSame( acquiredConnection1, acquiredConnection2 ); // reset & sync were called only when pooled connection was closed previously verify( connection ).reset(); verify( connection ).sync(); } @Test public void connectionThatWasIdleForALongTimeIsValidatedDuringAcquisition() { Connection connection = newConnectionMock( ADDRESS_1 ); long idleTimeBeforeConnectionTest = 100; FakeClock clock = new FakeClock(); SocketConnectionPool pool = newPool( newMockConnector( connection ), clock, idleTimeBeforeConnectionTest ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); verify( connection, never() ).reset(); verify( connection, never() ).sync(); // return to the pool acquiredConnection1.close(); verify( connection ).reset(); verify( connection ).sync(); clock.progress( idleTimeBeforeConnectionTest + 42 ); Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); assertSame( acquiredConnection1, acquiredConnection2 ); // reset & sync were called only when pooled connection was closed previously verify( connection, times( 2 ) ).reset(); verify( connection, times( 2 ) ).sync(); } @Test public void connectionThatWasIdleForALongTimeIsNotValidatedDuringAcquisitionWhenTimeoutNotConfigured() { Connection connection = newConnectionMock( ADDRESS_1 ); long idleTimeBeforeConnectionTest = PoolSettings.NO_IDLE_CONNECTION_TEST; FakeClock clock = new FakeClock(); SocketConnectionPool pool = newPool( newMockConnector( connection ), clock, idleTimeBeforeConnectionTest ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); verify( connection, never() ).reset(); verify( connection, never() ).sync(); // return to the pool acquiredConnection1.close(); verify( connection ).reset(); verify( connection ).sync(); clock.progress( 1000 ); Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); assertSame( acquiredConnection1, acquiredConnection2 ); verify( connection ).reset(); verify( connection ).sync(); } @Test public void brokenConnectionsSkippedDuringAcquisition() { Connection connection1 = newConnectionMock( ADDRESS_1 ); Connection connection2 = newConnectionMock( ADDRESS_1 ); Connection connection3 = newConnectionMock( ADDRESS_1 ); doNothing().doThrow( new RuntimeException( "failed to reset" ) ).when( connection1 ).reset(); doNothing().doThrow( new RuntimeException( "failed to sync" ) ).when( connection2 ).sync(); int idleTimeBeforeConnectionTest = 10; FakeClock clock = new FakeClock(); Connector connector = newMockConnector( connection1, connection2, connection3 ); SocketConnectionPool pool = newPool( connector, clock, idleTimeBeforeConnectionTest ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection3 = pool.acquire( ADDRESS_1 ); // return acquired connections to the pool acquiredConnection1.close(); acquiredConnection2.close(); acquiredConnection3.close(); clock.progress( idleTimeBeforeConnectionTest + 1 ); Connection acquiredConnection = pool.acquire( ADDRESS_1 ); acquiredConnection.reset(); acquiredConnection.sync(); assertSame( acquiredConnection3, acquiredConnection ); } @Test public void limitedNumberOfBrokenConnectionsIsSkippedDuringAcquisition() { Connection connection1 = newConnectionMock( ADDRESS_1 ); Connection connection2 = newConnectionMock( ADDRESS_1 ); Connection connection3 = newConnectionMock( ADDRESS_1 ); Connection connection4 = newConnectionMock( ADDRESS_1 ); doNothing().doThrow( new RuntimeException( "failed to reset 1" ) ).when( connection1 ).reset(); doNothing().doThrow( new RuntimeException( "failed to sync 2" ) ).when( connection2 ).sync(); doNothing().doThrow( new RuntimeException( "failed to reset 3" ) ).when( connection3 ).reset(); RuntimeException recentlyUsedConnectionFailure = new RuntimeException( "failed to sync 4" ); doNothing().doThrow( recentlyUsedConnectionFailure ).when( connection4 ).sync(); int idleTimeBeforeConnectionTest = 10; FakeClock clock = new FakeClock(); Connector connector = newMockConnector( connection1, connection2, connection3, connection4 ); SocketConnectionPool pool = newPool( connector, clock, idleTimeBeforeConnectionTest ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection3 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection4 = pool.acquire( ADDRESS_1 ); acquiredConnection1.close(); acquiredConnection2.close(); acquiredConnection3.close(); clock.progress( idleTimeBeforeConnectionTest + 1 ); acquiredConnection4.close(); Connection acquiredConnection = pool.acquire( ADDRESS_1 ); acquiredConnection.reset(); try { acquiredConnection.sync(); fail( "Exception expected" ); } catch ( Exception e ) { assertSame( recentlyUsedConnectionFailure, e ); } assertSame( acquiredConnection4, acquiredConnection ); } @Test public void acquireRetriesUntilAConnectionIsCreated() { Connection connection1 = newConnectionMock( ADDRESS_1 ); Connection connection2 = newConnectionMock( ADDRESS_1 ); Connection connection3 = newConnectionMock( ADDRESS_1 ); Connection connection4 = newConnectionMock( ADDRESS_1 ); doNothing().doThrow( new RuntimeException() ).when( connection1 ).reset(); doNothing().doThrow( new RuntimeException() ).when( connection2 ).reset(); doNothing().doThrow( new RuntimeException() ).when( connection3 ).reset(); int idleTimeBeforeConnectionTest = 10; FakeClock clock = new FakeClock(); Connector connector = newMockConnector( connection1, connection2, connection3, connection4 ); SocketConnectionPool pool = newPool( connector, clock, idleTimeBeforeConnectionTest ); Connection acquiredConnection1 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection2 = pool.acquire( ADDRESS_1 ); Connection acquiredConnection3 = pool.acquire( ADDRESS_1 ); acquiredConnection1.close(); acquiredConnection2.close(); acquiredConnection3.close(); // make all connections seem idle for too long clock.progress( idleTimeBeforeConnectionTest + 10 ); Connection acquiredConnection = pool.acquire( ADDRESS_1 ); assertThat( acquiredConnection, not( isOneOf( acquiredConnection1, acquiredConnection2, acquiredConnection3 ) ) ); // all connections were tested and appeared to be broken InOrder inOrder = inOrder( connection1, connection2, connection3, connection4 ); inOrder.verify( connection1 ).reset(); inOrder.verify( connection2 ).reset(); inOrder.verify( connection3 ).reset(); inOrder.verify( connection4, never() ).reset(); inOrder.verify( connection4, never() ).sync(); } private static Answer<Connection> createConnectionAnswer( final Set<Connection> createdConnections ) { return new Answer<Connection>() { @Override public Connection answer( InvocationOnMock invocation ) { BoltServerAddress address = invocation.getArgumentAt( 0, BoltServerAddress.class ); Connection connection = newConnectionMock( address ); createdConnections.add( connection ); return connection; } }; } private static Callable<Void> acquireConnection( final SocketConnectionPool pool, final AtomicInteger port ) { return new Callable<Void>() { @Override public Void call() throws Exception { while ( true ) { pool.acquire( new BoltServerAddress( "localhost", port.incrementAndGet() ) ); } } }; } private static Connector newMockConnector() { Connection connection = mock( Connection.class ); return newMockConnector( connection ); } private static Connector newMockConnector( Connection connection, Connection... otherConnections ) { Connector connector = mock( Connector.class ); when( connector.connect( any( BoltServerAddress.class ) ) ).thenReturn( connection, otherConnections ); return connector; } private static SocketConnectionPool newPool( Connector connector ) { return newPool( connector, Clock.SYSTEM, 0 ); } private static SocketConnectionPool newPool( Connector connector, Clock clock, long idleTimeBeforeConnectionTest ) { PoolSettings poolSettings = new PoolSettings( 42, idleTimeBeforeConnectionTest ); Logging logging = mock( Logging.class, RETURNS_MOCKS ); return new SocketConnectionPool( poolSettings, connector, clock, logging ); } private static Connection newConnectionMock( BoltServerAddress address ) { Connection connection = mock( Connection.class ); if ( address != null ) { when( connection.boltServerAddress() ).thenReturn( address ); } return connection; } }