Skip to content

Commit 7b9248d

Browse files
authored
Fast failing discovery on certain errors (#611)
The driver tries its best to fetch a routing table. It will try all possible routers while skipping routers on most errors. However, there are a few errors that are caused by the client. Those errors should be surfaced to the user for a better UX/DX and should fail fast: there is no reason to try another router if we expect it tho return the same error. Those errors are: - `Neo.ClientError.Database.DatabaseNotFound` - all `Neo.ClientError.Security.*` - except `Neo.ClientError.Security.AuthorizationExpired` - `Neo.ClientError.Transaction.InvalidBookmark` - `Neo.ClientError.Transaction.InvalidBookmarkMixture` This PR also changes auth errors to be properly hydrated when received as HELLO response.
1 parent b5cb8ee commit 7b9248d

File tree

8 files changed

+46
-91
lines changed

8 files changed

+46
-91
lines changed

neo4j/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,11 @@ def verify_connectivity(self, **config):
435435
return self._verify_routing_connectivity()
436436

437437
def _verify_routing_connectivity(self):
438-
from neo4j.exceptions import ServiceUnavailable
439-
from neo4j._exceptions import BoltHandshakeError
438+
from neo4j.exceptions import (
439+
Neo4jError,
440+
ServiceUnavailable,
441+
SessionExpired,
442+
)
440443

441444
table = self._pool.get_routing_table_for_default_database()
442445
routing_info = {}
@@ -450,9 +453,8 @@ def _verify_routing_connectivity(self):
450453
timeout=self._default_workspace_config
451454
.connection_acquisition_timeout
452455
)
453-
except BoltHandshakeError as error:
456+
except (ServiceUnavailable, SessionExpired, Neo4jError):
454457
routing_info[ix] = None
455-
456458
for key, val in routing_info.items():
457459
if val is not None:
458460
return routing_info

neo4j/_exceptions.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,6 @@ class BoltTransactionError(BoltError):
102102
# TODO: pass the transaction object in as an argument
103103

104104

105-
class BoltRoutingError(BoltError):
106-
""" Raised when a fault occurs with obtaining a routing table.
107-
"""
108-
109-
110105
class BoltFailure(BoltError):
111106
""" Holds a Cypher failure.
112107
"""

neo4j/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,21 @@ def _extract_error_class(cls, classification, code):
130130
def invalidates_all_connections(self):
131131
return self.code == "Neo.ClientError.Security.AuthorizationExpired"
132132

133+
def is_fatal_during_discovery(self):
134+
# checks if the code is an error that is caused by the client. In this
135+
# case the driver should fail fast during discovery.
136+
if not isinstance(self.code, str):
137+
return False
138+
if self.code in ("Neo.ClientError.Database.DatabaseNotFound",
139+
"Neo.ClientError.Transaction.InvalidBookmark",
140+
"Neo.ClientError.Transaction.InvalidBookmarkMixture"):
141+
return True
142+
if (self.code.startswith("Neo.ClientError.Security.")
143+
and self.code != "Neo.ClientError.Security."
144+
"AuthorizationExpired"):
145+
return True
146+
return False
147+
133148
def __str__(self):
134149
return "{{code: {code}}} {{message: {message}}}".format(code=self.code, message=self.message)
135150

neo4j/io/__init__.py

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
BoltError,
6767
BoltHandshakeError,
6868
BoltProtocolError,
69-
BoltRoutingError,
7069
BoltSecurityError,
7170
)
7271
from neo4j.addressing import Address
@@ -937,35 +936,15 @@ def fetch_routing_info(self, address, database, imp_user, bookmarks,
937936
:raise ServiceUnavailable: if the server does not support
938937
routing, or if routing support is broken or outdated
939938
"""
939+
cx = self._acquire(address, timeout)
940940
try:
941-
cx = self._acquire(address, timeout)
942-
try:
943-
routing_table = cx.route(
944-
database or self.workspace_config.database,
945-
imp_user or self.workspace_config.impersonated_user,
946-
bookmarks
947-
)
948-
finally:
949-
self.release(cx)
950-
except BoltRoutingError as error:
951-
# Connection was successful, but routing support is
952-
# broken. This may indicate that the routing procedure
953-
# does not exist (for protocol versions prior to 4.3).
954-
# This error is converted into ServiceUnavailable,
955-
# therefore surfacing to the application as a signal that
956-
# routing is broken.
957-
log.debug("Routing is broken (%s)", error)
958-
raise ServiceUnavailable(*error.args)
959-
except (ServiceUnavailable, SessionExpired) as error:
960-
# The routing table request suffered a connection
961-
# failure. This should return a null routing table,
962-
# signalling to the caller to retry the request
963-
# elsewhere.
964-
log.debug("Routing is unavailable (%s)", error)
965-
routing_table = None
966-
# If the routing table is empty, deactivate the address.
967-
if not routing_table:
968-
self.deactivate(address)
941+
routing_table = cx.route(
942+
database or self.workspace_config.database,
943+
imp_user or self.workspace_config.impersonated_user,
944+
bookmarks
945+
)
946+
finally:
947+
self.release(cx)
969948
return routing_table
970949

971950
def fetch_routing_table(self, *, address, timeout, database, imp_user,
@@ -984,12 +963,19 @@ def fetch_routing_table(self, *, address, timeout, database, imp_user,
984963
:return: a new RoutingTable instance or None if the given router is
985964
currently unable to provide routing information
986965
"""
966+
new_routing_info = None
987967
try:
988968
new_routing_info = self.fetch_routing_info(
989969
address, database, imp_user, bookmarks, timeout
990970
)
971+
except Neo4jError as e:
972+
# checks if the code is an error that is caused by the client. In
973+
# this case there is no sense in trying to fetch a RT from another
974+
# router. Hence, the driver should fail fast during discovery.
975+
if e.is_fatal_during_discovery():
976+
raise
991977
except (ServiceUnavailable, SessionExpired):
992-
new_routing_info = None
978+
pass
993979
if not new_routing_info:
994980
log.debug("Failed to fetch routing info %s", address)
995981
return None

neo4j/io/_bolt3.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,21 +186,14 @@ def route(self, database=None, imp_user=None, bookmarks=None):
186186
metadata = {}
187187
records = []
188188

189-
def fail(md):
190-
from neo4j._exceptions import BoltRoutingError
191-
if md.get("code") == "Neo.ClientError.Procedure.ProcedureNotFound":
192-
raise BoltRoutingError("Server does not support routing", self.unresolved_address)
193-
else:
194-
raise BoltRoutingError("Routing support broken on server", self.unresolved_address)
195-
196189
# Ignoring database and bookmarks because there is no multi-db support.
197190
# The bookmarks are only relevant for making sure a previously created
198191
# db exists before querying a routing table for it.
199192
self.run(
200193
"CALL dbms.cluster.routing.getRoutingTable($context)", # This is an internal procedure call. Only available if the Neo4j 3.5 is setup with clustering.
201194
{"context": self.routing_context},
202195
mode="r", # Bolt Protocol Version(3, 0) supports mode="r"
203-
on_success=metadata.update, on_failure=fail
196+
on_success=metadata.update
204197
)
205198
self.pull(on_success=metadata.update, on_records=records.extend)
206199
self.send_all()

neo4j/io/_bolt4.py

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -135,24 +135,14 @@ def route(self, database=None, imp_user=None, bookmarks=None):
135135
metadata = {}
136136
records = []
137137

138-
def fail(md):
139-
from neo4j._exceptions import BoltRoutingError
140-
code = md.get("code")
141-
if code == "Neo.ClientError.Database.DatabaseNotFound":
142-
return # surface this error to the user
143-
elif code == "Neo.ClientError.Procedure.ProcedureNotFound":
144-
raise BoltRoutingError("Server does not support routing", self.unresolved_address)
145-
else:
146-
raise BoltRoutingError("Routing support broken on server", self.unresolved_address)
147-
148138
if database is None: # default database
149139
self.run(
150140
"CALL dbms.routing.getRoutingTable($context)",
151141
{"context": self.routing_context},
152142
mode="r",
153143
bookmarks=bookmarks,
154144
db=SYSTEM_DATABASE,
155-
on_success=metadata.update, on_failure=fail
145+
on_success=metadata.update
156146
)
157147
else:
158148
self.run(
@@ -161,7 +151,7 @@ def fail(md):
161151
mode="r",
162152
bookmarks=bookmarks,
163153
db=SYSTEM_DATABASE,
164-
on_success=metadata.update, on_failure=fail
154+
on_success=metadata.update
165155
)
166156
self.pull(on_success=metadata.update, on_records=records.extend)
167157
self.send_all()
@@ -409,18 +399,6 @@ def route(self, database=None, imp_user=None, bookmarks=None):
409399
)
410400
)
411401

412-
def fail(md):
413-
from neo4j._exceptions import BoltRoutingError
414-
code = md.get("code")
415-
if code == "Neo.ClientError.Database.DatabaseNotFound":
416-
return # surface this error to the user
417-
elif code == "Neo.ClientError.Procedure.ProcedureNotFound":
418-
raise BoltRoutingError("Server does not support routing",
419-
self.unresolved_address)
420-
else:
421-
raise BoltRoutingError("Routing support broken on server",
422-
self.unresolved_address)
423-
424402
routing_context = self.routing_context or {}
425403
log.debug("[#%04X] C: ROUTE %r %r %r", self.local_port,
426404
routing_context, bookmarks, database)
@@ -431,8 +409,7 @@ def fail(md):
431409
bookmarks = list(bookmarks)
432410
self._append(b"\x66", (routing_context, bookmarks, database),
433411
response=Response(self, "route",
434-
on_success=metadata.update,
435-
on_failure=fail))
412+
on_success=metadata.update))
436413
self.send_all()
437414
self.fetch_all()
438415
return [metadata.get("rt")]
@@ -476,18 +453,6 @@ class Bolt4x4(Bolt4x3):
476453
PROTOCOL_VERSION = Version(4, 4)
477454

478455
def route(self, database=None, imp_user=None, bookmarks=None):
479-
def fail(md):
480-
from neo4j._exceptions import BoltRoutingError
481-
code = md.get("code")
482-
if code == "Neo.ClientError.Database.DatabaseNotFound":
483-
return # surface this error to the user
484-
elif code == "Neo.ClientError.Procedure.ProcedureNotFound":
485-
raise BoltRoutingError("Server does not support routing",
486-
self.unresolved_address)
487-
else:
488-
raise BoltRoutingError("Routing support broken on server",
489-
self.unresolved_address)
490-
491456
routing_context = self.routing_context or {}
492457
db_context = {}
493458
if database is not None:
@@ -503,8 +468,7 @@ def fail(md):
503468
bookmarks = list(bookmarks)
504469
self._append(b"\x66", (routing_context, bookmarks, db_context),
505470
response=Response(self, "route",
506-
on_success=metadata.update,
507-
on_failure=fail))
471+
on_success=metadata.update))
508472
self.send_all()
509473
self.fetch_all()
510474
return [metadata.get("rt")]

neo4j/io/_common.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,12 @@ class InitResponse(Response):
242242

243243
def on_failure(self, metadata):
244244
code = metadata.get("code")
245-
message = metadata.get("message", "Connection initialisation failed")
246245
if code == "Neo.ClientError.Security.Unauthorized":
247-
raise AuthError(message)
246+
raise Neo4jError.hydrate(**metadata)
248247
else:
249-
raise ServiceUnavailable(message)
248+
raise ServiceUnavailable(
249+
metadata.get("message", "Connection initialisation failed")
250+
)
250251

251252

252253
class CommitResponse(Response):

tests/unit/test_exceptions.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
from neo4j._exceptions import (
5656
BoltError,
5757
BoltHandshakeError,
58-
BoltRoutingError,
5958
BoltConnectionError,
6059
BoltSecurityError,
6160
BoltConnectionBroken,

0 commit comments

Comments
 (0)