Skip to content

Commit b73820b

Browse files
committed
Abort discovery on bookmark failures and continue on authorization expired error (neo4j#1043)
This update ensures that discovery gets aborted on `ClientException` with the following codes: - `Neo.ClientError.Transaction.InvalidBookmark` - `Neo.ClientError.Transaction.InvalidBookmarkMixture` In addition, it makes sure that it continues on `AuthorizationExpiredException`. All security exceptions are mapped to `SecurityException`.
1 parent 57340ae commit b73820b

File tree

4 files changed

+110
-19
lines changed

4 files changed

+110
-19
lines changed

driver/src/main/java/org/neo4j/driver/internal/cluster/RediscoveryImpl.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
import org.neo4j.driver.Bookmark;
3535
import org.neo4j.driver.Logger;
36+
import org.neo4j.driver.exceptions.AuthorizationExpiredException;
37+
import org.neo4j.driver.exceptions.ClientException;
3638
import org.neo4j.driver.exceptions.DiscoveryException;
3739
import org.neo4j.driver.exceptions.FatalDiscoveryException;
3840
import org.neo4j.driver.exceptions.SecurityException;
@@ -59,6 +61,8 @@ public class RediscoveryImpl implements Rediscovery
5961
private static final String RECOVERABLE_DISCOVERY_ERROR_WITH_SERVER = "Received a recoverable discovery error with server '%s', " +
6062
"will continue discovery with other routing servers if available. " +
6163
"Complete failure is reported separately from this entry.";
64+
private static final String INVALID_BOOKMARK_CODE = "Neo.ClientError.Transaction.InvalidBookmark";
65+
private static final String INVALID_BOOKMARK_MIXTURE_CODE = "Neo.ClientError.Transaction.InvalidBookmarkMixture";
6266

6367
private final BoltServerAddress initialRouter;
6468
private final RoutingSettings settings;
@@ -279,9 +283,8 @@ private CompletionStage<ClusterComposition> lookupOnRouter( BoltServerAddress ro
279283
private ClusterComposition handleRoutingProcedureError( Throwable error, RoutingTable routingTable,
280284
BoltServerAddress routerAddress, Throwable baseError )
281285
{
282-
if ( error instanceof SecurityException || error instanceof FatalDiscoveryException )
286+
if ( mustAbortDiscovery( error ) )
283287
{
284-
// auth error or routing error happened, terminate the discovery procedure immediately
285288
throw new CompletionException( error );
286289
}
287290

@@ -295,6 +298,27 @@ private ClusterComposition handleRoutingProcedureError( Throwable error, Routing
295298
return null;
296299
}
297300

301+
private boolean mustAbortDiscovery( Throwable throwable )
302+
{
303+
boolean abort = false;
304+
305+
if ( !(throwable instanceof AuthorizationExpiredException) && throwable instanceof SecurityException )
306+
{
307+
abort = true;
308+
}
309+
else if ( throwable instanceof FatalDiscoveryException )
310+
{
311+
abort = true;
312+
}
313+
else if ( throwable instanceof ClientException )
314+
{
315+
String code = ((ClientException) throwable).code();
316+
abort = INVALID_BOOKMARK_CODE.equals( code ) || INVALID_BOOKMARK_MIXTURE_CODE.equals( code );
317+
}
318+
319+
return abort;
320+
}
321+
298322
@Override
299323
public List<BoltServerAddress> resolve() throws UnknownHostException
300324
{

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.neo4j.driver.exceptions.FatalDiscoveryException;
3232
import org.neo4j.driver.exceptions.Neo4jException;
3333
import org.neo4j.driver.exceptions.ResultConsumedException;
34+
import org.neo4j.driver.exceptions.SecurityException;
3435
import org.neo4j.driver.exceptions.ServiceUnavailableException;
3536
import org.neo4j.driver.exceptions.TransientException;
3637

@@ -64,25 +65,34 @@ public static ResultConsumedException newResultConsumedError()
6465

6566
public static Neo4jException newNeo4jError( String code, String message )
6667
{
67-
String classification = extractClassification( code );
68-
switch ( classification )
68+
switch ( extractErrorClass( code ) )
6969
{
7070
case "ClientError":
71-
if ( code.equalsIgnoreCase( "Neo.ClientError.Security.Unauthorized" ) )
71+
if ( "Security".equals( extractErrorSubClass( code ) ) )
7272
{
73-
return new AuthenticationException( code, message );
74-
}
75-
else if ( code.equalsIgnoreCase( "Neo.ClientError.Database.DatabaseNotFound" ) )
76-
{
77-
return new FatalDiscoveryException( code, message );
78-
}
79-
else if ( code.equalsIgnoreCase( "Neo.ClientError.Security.AuthorizationExpired" ) )
80-
{
81-
return new AuthorizationExpiredException( code, message );
73+
if ( code.equalsIgnoreCase( "Neo.ClientError.Security.Unauthorized" ) )
74+
{
75+
return new AuthenticationException( code, message );
76+
}
77+
else if ( code.equalsIgnoreCase( "Neo.ClientError.Security.AuthorizationExpired" ) )
78+
{
79+
return new AuthorizationExpiredException( code, message );
80+
}
81+
else
82+
{
83+
return new SecurityException( code, message );
84+
}
8285
}
8386
else
8487
{
85-
return new ClientException( code, message );
88+
if ( code.equalsIgnoreCase( "Neo.ClientError.Database.DatabaseNotFound" ) )
89+
{
90+
return new FatalDiscoveryException( code, message );
91+
}
92+
else
93+
{
94+
return new ClientException( code, message );
95+
}
8696
}
8797
case "TransientError":
8898
return new TransientException( code, message );
@@ -135,7 +145,7 @@ private static boolean isClientOrTransientError( Neo4jException error )
135145
return errorCode != null && (errorCode.contains( "ClientError" ) || errorCode.contains( "TransientError" ));
136146
}
137147

138-
private static String extractClassification( String code )
148+
private static String extractErrorClass( String code )
139149
{
140150
String[] parts = code.split( "\\." );
141151
if ( parts.length < 2 )
@@ -145,6 +155,16 @@ private static String extractClassification( String code )
145155
return parts[1];
146156
}
147157

158+
private static String extractErrorSubClass( String code )
159+
{
160+
String[] parts = code.split( "\\." );
161+
if ( parts.length < 3 )
162+
{
163+
return "";
164+
}
165+
return parts[2];
166+
}
167+
148168
public static void addSuppressed( Throwable mainError, Throwable error )
149169
{
150170
if ( mainError != error )

driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import io.netty.util.concurrent.GlobalEventExecutor;
2222
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
2325
import org.mockito.ArgumentCaptor;
2426

2527
import java.io.IOException;
@@ -32,6 +34,8 @@
3234

3335
import org.neo4j.driver.Logger;
3436
import org.neo4j.driver.exceptions.AuthenticationException;
37+
import org.neo4j.driver.exceptions.AuthorizationExpiredException;
38+
import org.neo4j.driver.exceptions.ClientException;
3539
import org.neo4j.driver.exceptions.DiscoveryException;
3640
import org.neo4j.driver.exceptions.ProtocolException;
3741
import org.neo4j.driver.exceptions.ServiceUnavailableException;
@@ -137,17 +141,59 @@ void shouldFailImmediatelyOnAuthError()
137141
RoutingTable table = routingTableMock( A, B, C );
138142

139143
AuthenticationException error = assertThrows( AuthenticationException.class,
140-
() -> await( rediscovery.lookupClusterComposition( table, pool, empty() ) ) );
144+
() -> await( rediscovery.lookupClusterComposition( table, pool, empty() ) ) );
141145
assertEquals( authError, error );
142146
verify( table ).forget( A );
143147
}
144148

149+
@Test
150+
void shouldUseAnotherRouterOnAuthorizationExpiredException()
151+
{
152+
ClusterComposition expectedComposition =
153+
new ClusterComposition( 42, asOrderedSet( A, B, C ), asOrderedSet( B, C, D ), asOrderedSet( A, B ) );
154+
155+
Map<BoltServerAddress,Object> responsesByAddress = new HashMap<>();
156+
responsesByAddress.put( A, new AuthorizationExpiredException( "Neo.ClientError.Security.AuthorizationExpired", "message" ) );
157+
responsesByAddress.put( B, expectedComposition );
158+
159+
ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress );
160+
Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) );
161+
RoutingTable table = routingTableMock( A, B, C );
162+
163+
ClusterComposition actualComposition = await( rediscovery.lookupClusterComposition( table, pool, empty() ) ).getClusterComposition();
164+
165+
assertEquals( expectedComposition, actualComposition );
166+
verify( table ).forget( A );
167+
verify( table, never() ).forget( B );
168+
verify( table, never() ).forget( C );
169+
}
170+
171+
@ParameterizedTest
172+
@ValueSource( strings = {"Neo.ClientError.Transaction.InvalidBookmark", "Neo.ClientError.Transaction.InvalidBookmarkMixture"} )
173+
void shouldFailImmediatelyOnBookmarkErrors( String code )
174+
{
175+
ClientException error = new ClientException( code, "Invalid" );
176+
177+
Map<BoltServerAddress,Object> responsesByAddress = new HashMap<>();
178+
responsesByAddress.put( A, new RuntimeException( "Hi!" ) );
179+
responsesByAddress.put( B, error );
180+
181+
ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress );
182+
Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) );
183+
RoutingTable table = routingTableMock( A, B, C );
184+
185+
ClientException actualError = assertThrows( ClientException.class,
186+
() -> await( rediscovery.lookupClusterComposition( table, pool, empty() ) ) );
187+
assertEquals( error, actualError );
188+
verify( table ).forget( A );
189+
}
190+
145191
@Test
146192
void shouldFallbackToInitialRouterWhenKnownRoutersFail()
147193
{
148194
BoltServerAddress initialRouter = A;
149195
ClusterComposition expectedComposition = new ClusterComposition( 42,
150-
asOrderedSet( C, B, A ), asOrderedSet( A, B ), asOrderedSet( D, E ) );
196+
asOrderedSet( C, B, A ), asOrderedSet( A, B ), asOrderedSet( D, E ) );
151197

152198
Map<BoltServerAddress,Object> responsesByAddress = new HashMap<>();
153199
responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); // first -> non-fatal failure

testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ public class GetFeatures implements TestkitRequest
4141
"Temporary:DriverFetchSize",
4242
"Temporary:DriverMaxTxRetryTime",
4343
"Feature:Auth:Kerberos",
44-
"Feature:Auth:Custom"
44+
"Feature:Auth:Custom",
45+
"Temporary:FastFailingDiscovery"
4546
) );
4647

4748
private static final Set<String> SYNC_FEATURES = new HashSet<>( Arrays.asList(

0 commit comments

Comments
 (0)