From 7d659f25abc986c63aa98e6b8fab504f0ed7a051 Mon Sep 17 00:00:00 2001 From: Dmitriy Tverdiakov <11927660+injectives@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:30:39 +0100 Subject: [PATCH] Remove stacktrace from connection acquisition attempts in LoadBalancer (#944) This is to reduce the noise in the logs on connection acquisition errors. Complete failures are reported separately. --- .../internal/cluster/RediscoveryImpl.java | 8 +- .../cluster/loadbalancing/LoadBalancer.java | 28 ++-- .../integration/RoutingDriverBoltKitTest.java | 132 ++++++++++-------- .../internal/cluster/RediscoveryTest.java | 12 +- 4 files changed, 105 insertions(+), 75 deletions(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java index 4c95288389..224624990e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java @@ -57,6 +57,9 @@ public class RediscoveryImpl implements Rediscovery { private static final String NO_ROUTERS_AVAILABLE = "Could not perform discovery for database '%s'. No routing server available."; private static final String RECOVERABLE_ROUTING_ERROR = "Failed to update routing table with server '%s'."; + private static final String RECOVERABLE_DISCOVERY_ERROR_WITH_SERVER = "Received a recoverable discovery error with server '%s', " + + "will continue discovery with other routing servers if available. " + + "Complete failure is reported separately from this entry."; private final BoltServerAddress initialRouter; private final RoutingSettings settings; @@ -258,8 +261,9 @@ private ClusterComposition handleRoutingProcedureError( Throwable error, Routing // Retriable error happened during discovery. DiscoveryException discoveryError = new DiscoveryException( format( RECOVERABLE_ROUTING_ERROR, routerAddress ), error ); Futures.combineErrors( baseError, discoveryError ); // we record each failure here - logger.warn( format( "Received a recoverable discovery error with server '%s', will continue discovery with other routing servers if available.", - routerAddress ), discoveryError ); + String warningMessage = format( RECOVERABLE_DISCOVERY_ERROR_WITH_SERVER, routerAddress ); + logger.warn( warningMessage ); + logger.debug( warningMessage, discoveryError ); routingTable.forget( routerAddress ); return null; } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java index 34b4dce032..946e5ae592 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java @@ -20,6 +20,7 @@ import io.netty.util.concurrent.EventExecutorGroup; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -60,6 +61,11 @@ public class LoadBalancer implements ConnectionProvider { private static final String LOAD_BALANCER_LOG_NAME = "LoadBalancer"; + private static final String CONNECTION_ACQUISITION_COMPLETION_FAILURE_MESSAGE = "Connection acquisition failed for all available addresses."; + private static final String CONNECTION_ACQUISITION_COMPLETION_EXCEPTION_MESSAGE = + "Failed to obtain connection towards %s server. Known routing table is: %s"; + private static final String CONNECTION_ACQUISITION_ATTEMPT_FAILURE_MESSAGE = + "Failed to obtain a connection towards address %s, will try other addresses if available. Complete failure is reported separately from this entry."; private final ConnectionPool connectionPool; private final RoutingTableRegistry routingTables; private final LoadBalancingStrategy loadBalancingStrategy; @@ -177,19 +183,23 @@ private CompletionStage acquire( AccessMode mode, RoutingTable routi { AddressSet addresses = addressSet( mode, routingTable ); CompletableFuture result = new CompletableFuture<>(); - acquire( mode, routingTable, addresses, result ); + List attemptExceptions = new ArrayList<>(); + acquire( mode, routingTable, addresses, result, attemptExceptions ); return result; } - private void acquire( AccessMode mode, RoutingTable routingTable, AddressSet addresses, CompletableFuture result ) + private void acquire( AccessMode mode, RoutingTable routingTable, AddressSet addresses, CompletableFuture result, + List attemptErrors ) { BoltServerAddress address = selectAddress( mode, addresses ); if ( address == null ) { - result.completeExceptionally( new SessionExpiredException( - "Failed to obtain connection towards " + mode + " server. " + - "Known routing table is: " + routingTable ) ); + SessionExpiredException completionError = + new SessionExpiredException( format( CONNECTION_ACQUISITION_COMPLETION_EXCEPTION_MESSAGE, mode, routingTable ) ); + attemptErrors.forEach( completionError::addSuppressed ); + log.error( CONNECTION_ACQUISITION_COMPLETION_FAILURE_MESSAGE, completionError ); + result.completeExceptionally( completionError ); return; } @@ -200,10 +210,12 @@ private void acquire( AccessMode mode, RoutingTable routingTable, AddressSet add { if ( error instanceof ServiceUnavailableException ) { - SessionExpiredException errorToLog = new SessionExpiredException( format( "Server at %s is no longer available", address ), error ); - log.warn( "Failed to obtain a connection towards address " + address, errorToLog ); + String attemptMessage = format( CONNECTION_ACQUISITION_ATTEMPT_FAILURE_MESSAGE, address ); + log.warn( attemptMessage ); + log.debug( attemptMessage, error ); + attemptErrors.add( error ); routingTable.forget( address ); - eventExecutorGroup.next().execute( () -> acquire( mode, routingTable, addresses, result ) ); + eventExecutorGroup.next().execute( () -> acquire( mode, routingTable, addresses, result, attemptErrors ) ); } else { diff --git a/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java index 8d7830010b..91a1fcfac9 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java +++ b/driver/src/test/java/org/neo4j/driver/integration/RoutingDriverBoltKitTest.java @@ -36,7 +36,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import org.neo4j.driver.AccessMode; import org.neo4j.driver.AuthToken; @@ -119,7 +118,7 @@ void shouldHandleAcquireReadSession() throws IOException, InterruptedException, URI uri = URI.create( "neo4j://127.0.0.1:9001" ); try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List result = session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); @@ -140,7 +139,7 @@ void shouldHandleAcquireReadTransaction() throws IOException, InterruptedExcepti StubServer readServer = stubController.startStub( "read_server_v3_read_tx.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { List result = session.readTransaction( tx -> tx.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ) ); @@ -162,7 +161,7 @@ void shouldHandleAcquireReadSessionAndTransaction() throws IOException, Interrup StubServer readServer = stubController.startStub( "read_server_v3_read_tx.script", 9005 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); Transaction tx = session.beginTransaction() ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); Transaction tx = session.beginTransaction() ) { List result = tx.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ); @@ -192,7 +191,7 @@ void shouldRoundRobinReadServers() throws IOException, InterruptedException, Stu try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { assertThat( session.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ), - equalTo( asList( "Bob", "Alice", "Tina" ) ) ); + equalTo( asList( "Bob", "Alice", "Tina" ) ) ); } } } @@ -218,10 +217,10 @@ void shouldRoundRobinReadServersWhenUsingTransaction() throws IOException, Inter for ( int i = 0; i < 2; i++ ) { try ( Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); - Transaction tx = session.beginTransaction() ) + Transaction tx = session.beginTransaction() ) { assertThat( tx.run( "MATCH (n) RETURN n.name" ).list( record -> record.get( "n.name" ).asString() ), - equalTo( asList( "Bob", "Alice", "Tina" ) ) ); + equalTo( asList( "Bob", "Alice", "Tina" ) ) ); tx.commit(); } } @@ -246,7 +245,7 @@ void shouldThrowSessionExpiredIfReadServerDisappears() throws IOException, Inter assertThrows( SessionExpiredException.class, () -> { try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { session.run( "MATCH (n) RETURN n.name" ); } @@ -269,8 +268,8 @@ void shouldThrowSessionExpiredIfReadServerDisappearsWhenUsingTransaction() throw SessionExpiredException e = assertThrows( SessionExpiredException.class, () -> { try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); - Transaction tx = session.beginTransaction() ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ); + Transaction tx = session.beginTransaction() ) { tx.run( "MATCH (n) RETURN n.name" ); tx.commit(); @@ -293,7 +292,7 @@ void shouldThrowSessionExpiredIfWriteServerDisappears() throws IOException, Inte //Expect try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { assertThrows( SessionExpiredException.class, () -> session.run( "CREATE (n {name:'Bob'})" ).consume() ); } @@ -316,7 +315,7 @@ void shouldThrowSessionExpiredIfWriteServerDisappearsWhenUsingTransaction() thro URI uri = URI.create( "neo4j://127.0.0.1:9001" ); //Expect try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) { assertThrows( SessionExpiredException.class, () -> tx.run( "MATCH (n) RETURN n.name" ).consume() ); } @@ -337,7 +336,7 @@ void shouldHandleAcquireWriteSession() throws IOException, InterruptedException, StubServer writeServer = stubController.startStub( "write_server_v3_write.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) ) { session.run( "CREATE (n {name:'Bob'})" ); } @@ -374,7 +373,7 @@ void shouldHandleAcquireWriteSessionAndTransaction() throws IOException, Interru StubServer writeServer = stubController.startStub( "write_server_v3_write_tx.script", 9007 ); URI uri = URI.create( "neo4j://127.0.0.1:9001" ); try ( Driver driver = GraphDatabase.driver( uri, INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ); Transaction tx = session.beginTransaction() ) { tx.run( "CREATE (n {name:'Bob'})" ); tx.commit(); @@ -558,18 +557,19 @@ void shouldHandleLeaderSwitchAndRetryWhenWritingInTxFunction() throws IOExceptio try ( Session session = driver.session( builder().withDatabase( "mydatabase" ).build() ) ) { - names = session.writeTransaction( tx -> - { - tx.run( "RETURN 1" ); - try - { - Thread.sleep( 100 ); - } - catch ( InterruptedException ex ) - { - } - return tx.run( "MATCH (n) RETURN n.name" ).list( RoutingDriverBoltKitTest::extractNameField ); - } ); + names = session.writeTransaction( + tx -> + { + tx.run( "RETURN 1" ); + try + { + Thread.sleep( 100 ); + } + catch ( InterruptedException ex ) + { + } + return tx.run( "MATCH (n) RETURN n.name" ).list( RoutingDriverBoltKitTest::extractNameField ); + } ); } assertEquals( asList( "Foo", "Bar" ), names ); @@ -594,16 +594,18 @@ void shouldHandleLeaderSwitchAndRetryWhenWritingInTxFunctionAsync() throws IOExc AsyncSession session = driver.asyncSession( builder().withDatabase( "mydatabase" ).build() ); List names = Futures.blockingGet( session.writeTransactionAsync( tx -> tx.runAsync( "RETURN 1" ) - .thenComposeAsync( ignored -> { - try - { - Thread.sleep( 100 ); - } - catch ( InterruptedException ex ) - { - } - return tx.runAsync( "MATCH (n) RETURN n.name" ); - } ) + .thenComposeAsync( + ignored -> + { + try + { + Thread.sleep( 100 ); + } + catch ( InterruptedException ex ) + { + } + return tx.runAsync( "MATCH (n) RETURN n.name" ); + } ) .thenComposeAsync( cursor -> cursor.listAsync( RoutingDriverBoltKitTest::extractNameField ) ) ) ); assertEquals( asList( "Foo", "Bar" ), names ); @@ -614,7 +616,7 @@ void shouldHandleLeaderSwitchAndRetryWhenWritingInTxFunctionAsync() throws IOExc assertThat( writeServer.exitStatus(), equalTo( 0 ) ); } - private static String extractNameField(Record record) + private static String extractNameField( Record record ) { return record.get( 0 ).asString(); } @@ -635,12 +637,15 @@ void shouldHandleLeaderSwitchAndRetryWhenWritingInTxFunctionRX() throws IOExcept Driver driver = GraphDatabase.driver( uri, Config.builder().withMaxTransactionRetryTime( 1, TimeUnit.MILLISECONDS ).build() ); Flux fluxOfNames = Flux.usingWhen( Mono.fromSupplier( () -> driver.rxSession( builder().withDatabase( "mydatabase" ).build() ) ), - session -> session.writeTransaction( tx -> - { - RxResult result = tx.run( "RETURN 1" ); - return Flux.from( result.records() ).limitRate( 100 ).thenMany( tx.run( "MATCH (n) RETURN n.name" ).records() ).limitRate( 100 ).map( - RoutingDriverBoltKitTest::extractNameField ); - } ), RxSession::close ); + session -> session.writeTransaction( + tx -> + { + RxResult result = tx.run( "RETURN 1" ); + return Flux.from( result.records() ).limitRate( 100 ).thenMany( + tx.run( "MATCH (n) RETURN n.name" ).records() ) + .limitRate( 100 ).map( + RoutingDriverBoltKitTest::extractNameField ); + } ), RxSession::close ); StepVerifier.create( fluxOfNames ).expectNext( "Foo", "Bar" ).verifyComplete(); @@ -657,7 +662,7 @@ void shouldSendInitialBookmark() throws Exception StubServer writer = stubController.startStub( "write_tx_with_bookmarks.script", 9007 ); try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); - Session session = driver.session( builder().withBookmarks( parse( "OldBookmark" ) ).build() ) ) + Session session = driver.session( builder().withBookmarks( parse( "OldBookmark" ) ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -679,7 +684,7 @@ void shouldUseWriteSessionModeAndInitialBookmark() throws Exception StubServer writer = stubController.startStub( "write_tx_with_bookmarks.script", 9008 ); try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( parse( "OldBookmark" ) ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( parse( "OldBookmark" ) ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -701,7 +706,7 @@ void shouldUseReadSessionModeAndInitialBookmark() throws Exception StubServer writer = stubController.startStub( "read_tx_with_bookmarks.script", 9005 ); try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withBookmarks( parse( "OldBookmark" ) ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).withBookmarks( parse( "OldBookmark" ) ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -726,7 +731,7 @@ void shouldPassBookmarkFromTransactionToTransaction() throws Exception StubServer writer = stubController.startStub( "write_read_tx_with_bookmarks.script", 9007 ); try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); - Session session = driver.session( builder().withBookmarks( parse( "BookmarkA" ) ).build() ) ) + Session session = driver.session( builder().withBookmarks( parse( "BookmarkA" ) ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -827,7 +832,9 @@ void shouldRetryWriteTransactionUntilSuccessWithWhenLeaderIsRemoved() throws Exc assertEquals( 0, writer.exitStatus() ); } verify( logger, times( 3 ) ).warn( startsWith( "Transaction failed and will be retried in" ), any( SessionExpiredException.class ) ); - verify( logger ).warn( startsWith( "Failed to obtain a connection towards address 127.0.0.1:9004" ), any( SessionExpiredException.class ) ); + String messageBeginning = "Failed to obtain a connection towards address 127.0.0.1:9004"; + verify( logger ).warn( startsWith( messageBeginning ) ); + verify( logger ).debug( startsWith( messageBeginning ), any( ServiceUnavailableException.class ) ); } @Test @@ -861,7 +868,9 @@ void shouldRetryWriteTransactionUntilSuccessWithWhenLeaderIsRemovedV3() throws E } verify( logger, times( 1 ) ).warn( startsWith( "Transaction failed and will be retried in" ), any( TransientException.class ) ); verify( logger, times( 2 ) ).warn( startsWith( "Transaction failed and will be retried in" ), any( SessionExpiredException.class ) ); - verify( logger ).warn( startsWith( "Failed to obtain a connection towards address 127.0.0.1:9004" ), any( SessionExpiredException.class ) ); + String messageBeginning = "Failed to obtain a connection towards address 127.0.0.1:9004"; + verify( logger ).warn( startsWith( messageBeginning ) ); + verify( logger ).debug( startsWith( messageBeginning ), any( ServiceUnavailableException.class ) ); } @Test @@ -1127,7 +1136,7 @@ void shouldTreatRoutingTableWithSingleRouterAsValid() throws Exception StubServer reader2 = stubController.startStub( "read_server_v3_read.script", 9004 ); try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9010", INSECURE_CONFIG ); - Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) + Session session = driver.session( builder().withDefaultAccessMode( AccessMode.READ ).build() ) ) { // returned routing table contains only one router, this should be fine and we should be able to // read multiple times without additional rediscovery @@ -1156,7 +1165,7 @@ void shouldSendMultipleBookmarks() throws Exception try ( Driver driver = GraphDatabase.driver( "neo4j://127.0.0.1:9001", INSECURE_CONFIG ); Session session = driver.session( builder().withBookmarks( InternalBookmark.parse( asOrderedSet( "neo4j:bookmark:v1:tx5", "neo4j:bookmark:v1:tx29", "neo4j:bookmark:v1:tx94", "neo4j:bookmark:v1:tx56", - "neo4j:bookmark:v1:tx16", "neo4j:bookmark:v1:tx68" ) ) ).build() ) ) + "neo4j:bookmark:v1:tx16", "neo4j:bookmark:v1:tx68" ) ) ).build() ) ) { try ( Transaction tx = session.beginTransaction() ) { @@ -1393,16 +1402,17 @@ private static TransactionWork> queryWork( final String query, fina private static List readStrings( final String query, Session session ) { - return session.readTransaction( tx -> - { - List records = tx.run( query ).list(); - List names = new ArrayList<>( records.size() ); - for ( Record record : records ) - { - names.add( record.get( 0 ).asString() ); - } - return names; - } ); + return session.readTransaction( + tx -> + { + List records = tx.run( query ).list(); + List names = new ArrayList<>( records.size() ); + for ( Record record : records ) + { + names.add( record.get( 0 ).asString() ); + } + return names; + } ); } static class PortBasedServerAddressComparator implements Comparator diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java index 26656699ff..d8416f55ef 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java @@ -52,7 +52,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.startsWith; @@ -182,9 +181,14 @@ void shouldFailImmediatelyWhenClusterCompositionProviderReturnsFailure() ClusterComposition composition = await( rediscovery.lookupClusterComposition( table, pool, empty() ) ); assertEquals( validComposition, composition ); - ArgumentCaptor argument = ArgumentCaptor.forClass( DiscoveryException.class ); - verify( logger ).warn( anyString(), argument.capture() ); - assertThat( argument.getValue().getCause(), equalTo( protocolError ) ); + ArgumentCaptor warningMessageCaptor = ArgumentCaptor.forClass( String.class ); + ArgumentCaptor debugMessageCaptor = ArgumentCaptor.forClass( String.class ); + ArgumentCaptor debugThrowableCaptor = ArgumentCaptor.forClass( DiscoveryException.class ); + verify( logger ).warn( warningMessageCaptor.capture() ); + verify( logger ).debug( debugMessageCaptor.capture(), debugThrowableCaptor.capture() ); + assertNotNull( warningMessageCaptor.getValue() ); + assertEquals( warningMessageCaptor.getValue(), debugMessageCaptor.getValue() ); + assertThat( debugThrowableCaptor.getValue().getCause(), equalTo( protocolError ) ); } @Test