Skip to content

Commit 346c1d2

Browse files
authored
Merge pull request #538 from lutovich/1.7-async-stacktraces
Improve stacktraces for errors from blocking API
2 parents d6f1f86 + 7a79d06 commit 346c1d2

File tree

4 files changed

+86
-6
lines changed

4 files changed

+86
-6
lines changed

driver/src/main/java/org/neo4j/driver/internal/util/ErrorUtil.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
*/
1919
package org.neo4j.driver.internal.util;
2020

21+
import io.netty.util.internal.PlatformDependent;
22+
23+
import java.util.concurrent.ExecutionException;
24+
import java.util.stream.Stream;
25+
2126
import org.neo4j.driver.v1.exceptions.AuthenticationException;
2227
import org.neo4j.driver.v1.exceptions.ClientException;
2328
import org.neo4j.driver.v1.exceptions.DatabaseException;
@@ -85,6 +90,21 @@ public static boolean isFatal( Throwable error )
8590
return true;
8691
}
8792

93+
public static void rethrowAsyncException( ExecutionException e )
94+
{
95+
Throwable error = e.getCause();
96+
97+
InternalExceptionCause internalCause = new InternalExceptionCause( error.getStackTrace() );
98+
error.addSuppressed( internalCause );
99+
100+
StackTraceElement[] currentStackTrace = Stream.of( Thread.currentThread().getStackTrace() )
101+
.skip( 2 ) // do not include Thread.currentThread() and this method in the stacktrace
102+
.toArray( StackTraceElement[]::new );
103+
error.setStackTrace( currentStackTrace );
104+
105+
PlatformDependent.throwException( error );
106+
}
107+
88108
private static boolean isProtocolViolationError( Neo4jException error )
89109
{
90110
String errorCode = error.code();
@@ -106,4 +126,24 @@ private static String extractClassification( String code )
106126
}
107127
return parts[1];
108128
}
129+
130+
/**
131+
* Exception which is merely a holder of an async stacktrace, which is not the primary stacktrace users are interested in.
132+
* Used for blocking API calls that block on async API calls.
133+
*/
134+
private static class InternalExceptionCause extends RuntimeException
135+
{
136+
InternalExceptionCause( StackTraceElement[] stackTrace )
137+
{
138+
setStackTrace( stackTrace );
139+
}
140+
141+
@Override
142+
public synchronized Throwable fillInStackTrace()
143+
{
144+
// no need to fill in the stack trace
145+
// this exception just uses the given stack trace
146+
return this;
147+
}
148+
}
109149
}

driver/src/main/java/org/neo4j/driver/internal/util/Futures.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
*/
1919
package org.neo4j.driver.internal.util;
2020

21-
import io.netty.util.internal.PlatformDependent;
22-
2321
import java.util.concurrent.CompletableFuture;
2422
import java.util.concurrent.CompletionException;
2523
import java.util.concurrent.CompletionStage;
@@ -121,7 +119,7 @@ public static <V> V blockingGet( CompletionStage<V> stage, Runnable interruptHan
121119
}
122120
catch ( ExecutionException e )
123121
{
124-
PlatformDependent.throwException( e.getCause() );
122+
ErrorUtil.rethrowAsyncException( e );
125123
}
126124
}
127125
}

driver/src/test/java/org/neo4j/driver/v1/integration/ErrorIT.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@
2020

2121
import io.netty.channel.Channel;
2222
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.TestInfo;
2324
import org.junit.jupiter.api.extension.RegisterExtension;
2425

2526
import java.io.IOException;
27+
import java.lang.reflect.Method;
2628
import java.net.URI;
29+
import java.util.Arrays;
2730
import java.util.List;
31+
import java.util.Objects;
2832
import java.util.UUID;
2933
import java.util.function.Consumer;
34+
import java.util.stream.Stream;
3035

3136
import org.neo4j.driver.internal.cluster.RoutingSettings;
3237
import org.neo4j.driver.internal.messaging.response.FailureMessage;
@@ -46,10 +51,13 @@
4651
import org.neo4j.driver.v1.exceptions.ServiceUnavailableException;
4752
import org.neo4j.driver.v1.util.SessionExtension;
4853

54+
import static java.util.Arrays.asList;
4955
import static java.util.concurrent.TimeUnit.SECONDS;
5056
import static org.hamcrest.CoreMatchers.equalTo;
5157
import static org.hamcrest.CoreMatchers.startsWith;
5258
import static org.hamcrest.Matchers.containsString;
59+
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
60+
import static org.hamcrest.Matchers.hasSize;
5361
import static org.hamcrest.Matchers.instanceOf;
5462
import static org.hamcrest.junit.MatcherAssert.assertThat;
5563
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -228,6 +236,20 @@ void shouldCloseChannelOnInboundFatalFailureMessage() throws InterruptedExceptio
228236
assertEquals( queryError.getMessage(), errorMessage );
229237
}
230238

239+
@Test
240+
void shouldThrowErrorWithNiceStackTrace( TestInfo testInfo )
241+
{
242+
ClientException error = assertThrows( ClientException.class, () -> session.run( "RETURN 10 / 0" ).consume() );
243+
244+
// thrown error should have this class & method in the stacktrace
245+
StackTraceElement[] stackTrace = error.getStackTrace();
246+
assertTrue( Stream.of( stackTrace ).anyMatch( element -> testClassAndMethodMatch( testInfo, element ) ),
247+
() -> "Expected stacktrace element is absent:\n" + Arrays.toString( stackTrace ) );
248+
249+
// thrown error should have a suppressed error with an async stacktrace
250+
assertThat( asList( error.getSuppressed() ), hasSize( greaterThanOrEqualTo( 1 ) ) );
251+
}
252+
231253
private Throwable testChannelErrorHandling( Consumer<FailingMessageFormat> messageFormatSetup )
232254
throws InterruptedException
233255
{
@@ -276,4 +298,23 @@ private void assertNewQueryCanBeExecuted( Session session, ChannelTrackingDriver
276298
Channel lastChannel = channels.get( channels.size() - 1 );
277299
assertTrue( lastChannel.isActive() );
278300
}
301+
302+
private static boolean testClassAndMethodMatch( TestInfo testInfo, StackTraceElement element )
303+
{
304+
return testClassMatches( testInfo, element ) && testMethodMatches( testInfo, element );
305+
}
306+
307+
private static boolean testClassMatches( TestInfo testInfo, StackTraceElement element )
308+
{
309+
String expectedName = testInfo.getTestClass().map( Class::getName ).orElse( "" );
310+
String actualName = element.getClassName();
311+
return Objects.equals( expectedName, actualName );
312+
}
313+
314+
private static boolean testMethodMatches( TestInfo testInfo, StackTraceElement element )
315+
{
316+
String expectedName = testInfo.getTestMethod().map( Method::getName ).orElse( "" );
317+
String actualName = element.getMethodName();
318+
return Objects.equals( expectedName, actualName );
319+
}
279320
}

driver/src/test/java/org/neo4j/driver/v1/integration/TransactionIT.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.junit.jupiter.api.Test;
2323
import org.junit.jupiter.api.extension.RegisterExtension;
2424

25-
import java.util.Arrays;
2625
import java.util.List;
2726
import java.util.Map;
2827

@@ -45,9 +44,11 @@
4544
import org.neo4j.driver.v1.util.StubServer;
4645
import org.neo4j.driver.v1.util.TestUtil;
4746

47+
import static java.util.Arrays.asList;
4848
import static org.hamcrest.CoreMatchers.containsString;
4949
import static org.hamcrest.CoreMatchers.equalTo;
5050
import static org.hamcrest.CoreMatchers.startsWith;
51+
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
5152
import static org.hamcrest.Matchers.instanceOf;
5253
import static org.hamcrest.junit.MatcherAssert.assertThat;
5354
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -368,7 +369,7 @@ void shouldDisallowQueriesAfterFailureWhenResultsAreConsumed()
368369
try ( Transaction tx = session.beginTransaction() )
369370
{
370371
List<Integer> xs = tx.run( "UNWIND [1,2,3] AS x CREATE (:Node) RETURN x" ).list( record -> record.get( 0 ).asInt() );
371-
assertEquals( Arrays.asList( 1, 2, 3 ), xs );
372+
assertEquals( asList( 1, 2, 3 ), xs );
372373

373374
ClientException error1 = assertThrows( ClientException.class, () -> tx.run( "RETURN unknown" ).consume() );
374375
assertThat( error1.code(), containsString( "SyntaxError" ) );
@@ -404,7 +405,7 @@ void shouldRollbackWhenMarkedSuccessfulButOneStatementFails()
404405
} );
405406

406407
assertThat( error.code(), containsString( "SyntaxError" ) );
407-
assertEquals( 1, error.getSuppressed().length );
408+
assertThat( error.getSuppressed().length, greaterThanOrEqualTo( 1 ) );
408409
Throwable suppressed = error.getSuppressed()[0];
409410
assertThat( suppressed, instanceOf( ClientException.class ) );
410411
assertThat( suppressed.getMessage(), startsWith( "Transaction can't be committed" ) );

0 commit comments

Comments
 (0)