Skip to content

Commit aeeb185

Browse files
4.0 error handling improvements (#366)
* added new errors ResultConsumedError TransactionNestingError * added unit tests for exceptions testing ServiceUnavailable and Nesting BoltProtocolError into a ServiceUnavailable error. * added exceptions and unit tests ConfigurationError AuthConfigurationError CertificateConfigurationError * updated error propagation The BoltHandshakeError is wrapped in a ServiceUnavailable error. Removed exposure of ServiceUnavailable in neo4j/__init__.py * raises BoltHandshake error instead of ServiceUnavailable * fixed integration test code * improved speed for stub tests * improved speed and fixed http error with xfail
1 parent 61744ee commit aeeb185

27 files changed

+460
-184
lines changed

neo4j/__init__.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
SessionConfig,
7474
unit_of_work,
7575
)
76-
from neo4j.exceptions import ServiceUnavailable
76+
7777

7878
log = getLogger("neo4j")
7979

@@ -94,12 +94,10 @@ def driver(cls, uri, *, auth=None, acquire_timeout=None, **config):
9494
"""
9595
parsed = urlparse(uri)
9696
if parsed.scheme == "bolt":
97-
return cls.bolt_driver(parsed.netloc, auth=auth, acquire_timeout=acquire_timeout,
98-
**config)
97+
return cls.bolt_driver(parsed.netloc, auth=auth, acquire_timeout=acquire_timeout, **config)
9998
elif parsed.scheme == "neo4j" or parsed.scheme == "bolt+routing":
10099
rc = cls._parse_routing_context(parsed.query)
101-
return cls.neo4j_driver(parsed.netloc, auth=auth, routing_context=rc,
102-
acquire_timeout=acquire_timeout, **config)
100+
return cls.neo4j_driver(parsed.netloc, auth=auth, routing_context=rc, acquire_timeout=acquire_timeout, **config)
103101
else:
104102
raise ValueError("Unknown URI scheme {!r}".format(parsed.scheme))
105103

@@ -108,16 +106,27 @@ def bolt_driver(cls, target, *, auth=None, acquire_timeout=None, **config):
108106
""" Create a driver for direct Bolt server access that uses
109107
socket I/O and thread-based concurrency.
110108
"""
111-
return BoltDriver.open(target, auth=auth, acquire_timeout=acquire_timeout, **config)
109+
from neo4j._exceptions import BoltHandshakeError
110+
111+
try:
112+
return BoltDriver.open(target, auth=auth, acquire_timeout=acquire_timeout, **config)
113+
except BoltHandshakeError as error:
114+
from neo4j.exceptions import ServiceUnavailable
115+
raise ServiceUnavailable(str(error)) from error
112116

113117
@classmethod
114118
def neo4j_driver(cls, *targets, auth=None, routing_context=None, acquire_timeout=None,
115119
**config):
116120
""" Create a driver for routing-capable Neo4j service access
117121
that uses socket I/O and thread-based concurrency.
118122
"""
119-
return Neo4jDriver.open(*targets, auth=auth, routing_context=routing_context,
120-
acquire_timeout=acquire_timeout, **config)
123+
from neo4j._exceptions import BoltHandshakeError
124+
125+
try:
126+
return Neo4jDriver.open(*targets, auth=auth, routing_context=routing_context, acquire_timeout=acquire_timeout, **config)
127+
except BoltHandshakeError as error:
128+
from neo4j.exceptions import ServiceUnavailable
129+
raise ServiceUnavailable(str(error)) from error
121130

122131
@classmethod
123132
def _parse_routing_context(cls, query):
@@ -340,14 +349,23 @@ def get_routing_table(self):
340349
return self._pool.routing_table
341350

342351
def verify_connectivity(self, **config):
352+
"""
353+
:raise ServiceUnavailable: raised if the server does not support routing or if routing support is broken.
354+
"""
343355
# TODO: Improve and update Stub Test Server to be able to test.
344356
return self._verify_routing_connectivity()
345357

346358
def _verify_routing_connectivity(self):
359+
from neo4j.exceptions import ServiceUnavailable
360+
from neo4j._exceptions import BoltHandshakeError
361+
347362
table = self.get_routing_table()
348363
routing_info = {}
349364
for ix in list(table.routers):
350-
routing_info[ix] = self._pool.fetch_routing_info(table.routers[0])
365+
try:
366+
routing_info[ix] = self._pool.fetch_routing_info(table.routers[0])
367+
except BoltHandshakeError as error:
368+
routing_info[ix] = None
351369

352370
for key, val in routing_info.items():
353371
if val is not None:

neo4j/exceptions.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
4040
+ DriverError
4141
+ TransactionError
42+
+ TransactionNestingError
4243
+ SessionExpired
4344
+ ServiceUnavailable
4445
+ RoutingServiceUnavailable
@@ -47,7 +48,7 @@
4748
+ ConfigurationError
4849
+ AuthConfigurationError
4950
+ CertificateConfigurationError
50-
51+
+ ResultConsumedError
5152
5253
Connector API Errors
5354
====================
@@ -65,6 +66,10 @@
6566
6667
"""
6768

69+
CLASSIFICATION_CLIENT = "ClientError"
70+
CLASSIFICATION_TRANSIENT = "TransientError"
71+
CLASSIFICATION_DATABASE = "DatabaseError"
72+
6873

6974
class Neo4jError(Exception):
7075
""" Raised when the Cypher engine returns an error to the client.
@@ -79,12 +84,12 @@ class Neo4jError(Exception):
7984

8085
@classmethod
8186
def hydrate(cls, message=None, code=None, **metadata):
82-
message = message or "An unknown error occurred."
87+
message = message or "An unknown error occurred"
8388
code = code or "Neo.DatabaseError.General.UnknownError"
8489
try:
8590
_, classification, category, title = code.split(".")
8691
except ValueError:
87-
classification = "DatabaseError"
92+
classification = CLASSIFICATION_DATABASE
8893
category = "General"
8994
title = "UnknownError"
9095

@@ -101,19 +106,19 @@ def hydrate(cls, message=None, code=None, **metadata):
101106

102107
@classmethod
103108
def _extract_error_class(cls, classification, code):
104-
if classification == "ClientError":
109+
if classification == CLASSIFICATION_CLIENT:
105110
try:
106111
return client_errors[code]
107112
except KeyError:
108113
return ClientError
109114

110-
elif classification == "TransientError":
115+
elif classification == CLASSIFICATION_TRANSIENT:
111116
try:
112117
return transient_errors[code]
113118
except KeyError:
114119
return TransientError
115120

116-
elif classification == "DatabaseError":
121+
elif classification == CLASSIFICATION_DATABASE:
117122
return DatabaseError
118123

119124
else:
@@ -238,6 +243,15 @@ def __init__(self, transaction, *args, **kwargs):
238243
self.transaction = transaction
239244

240245

246+
class TransactionNestingError(DriverError):
247+
""" Raised when transactions are nested incorrectly.
248+
"""
249+
250+
def __init__(self, transaction, *args, **kwargs):
251+
super(TransactionError, self).__init__(*args, **kwargs)
252+
self.transaction = transaction
253+
254+
241255
class ServiceUnavailable(DriverError):
242256
""" Raised when no database service is available.
243257
"""
@@ -256,3 +270,23 @@ class WriteServiceUnavailable(ServiceUnavailable):
256270
class ReadServiceUnavailable(ServiceUnavailable):
257271
""" Raised when no read service is available.
258272
"""
273+
274+
275+
class ResultConsumedError(DriverError):
276+
""" Raised when trying to access records after the records have been consumed.
277+
"""
278+
279+
280+
class ConfigurationError(DriverError):
281+
""" Raised when there is an error concerning a configuration.
282+
"""
283+
284+
285+
class AuthConfigurationError(ConfigurationError):
286+
""" Raised when there is an error with the authentication configuration.
287+
"""
288+
289+
290+
class CertificateConfigurationError(ConfigurationError):
291+
""" Raised when there is an error with the authentication configuration.
292+
"""

neo4j/io/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ def ping(cls, address, *, timeout=None, **config):
154154
s, protocol_version = connect(address, timeout=timeout, config=config)
155155
except ServiceUnavailable:
156156
return None
157+
except BoltHandshakeError as e:
158+
return None
157159
else:
158160
s.close()
159161
return protocol_version
@@ -167,6 +169,8 @@ def open(cls, address, *, auth=None, timeout=None, **config):
167169
:param timeout:
168170
:param config:
169171
:return:
172+
:raise BoltHandshakeError: raised if the Bolt Protocol can not negotiate a protocol version.
173+
:raise ServiceUnavailable: raised if there was a connection issue.
170174
"""
171175
config = PoolConfig.consume(config)
172176
s, config.protocol_version, handshake, data = connect(address, timeout=timeout, config=config)
@@ -839,8 +843,7 @@ def _handshake(s, resolved_address):
839843
# response, the server has closed the connection
840844
log.debug("[#%04X] S: <CLOSE>", local_port)
841845
s.close()
842-
raise ServiceUnavailable("Connection to %r closed without handshake "
843-
"response" % (resolved_address,))
846+
raise BoltHandshakeError("Connection to {address} closed without handshake response".format(address=resolved_address), address=resolved_address, request_data=handshake, response_data=None)
844847
if data_size != 4:
845848
# Some garbled data has been received
846849
log.debug("[#%04X] S: @*#!", local_port)

tests/integration/conftest.py

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
from os.path import dirname, join
2525
from threading import RLock
2626

27-
from pytest import fixture, skip
27+
import pytest
28+
import urllib
2829

29-
from neo4j import GraphDatabase
30+
from neo4j import (
31+
GraphDatabase,
32+
)
3033
from neo4j.exceptions import ServiceUnavailable
3134
from neo4j._exceptions import BoltHandshakeError
3235
from neo4j.io import Bolt
3336

3437

38+
3539
NEO4J_RELEASES = getenv("NEO4J_RELEASES", "snapshot-enterprise 3.5-enterprise").split()
3640
NEO4J_HOST = "localhost"
3741
NEO4J_PORTS = {
@@ -170,7 +174,7 @@ def stop(self, timeout=None):
170174
NEO4J_RELEASES = ["existing"]
171175

172176

173-
@fixture(scope="session", params=NEO4J_RELEASES)
177+
@pytest.fixture(scope="session", params=NEO4J_RELEASES)
174178
def service(request):
175179
global NEO4J_SERVICE
176180
if NEO4J_DEBUG:
@@ -182,16 +186,19 @@ def service(request):
182186
if existing_service:
183187
NEO4J_SERVICE = existing_service
184188
else:
185-
NEO4J_SERVICE = Neo4jService(auth=NEO4J_AUTH, image=request.param,
186-
n_cores=NEO4J_CORES, n_replicas=NEO4J_REPLICAS)
187-
NEO4J_SERVICE.start(timeout=300)
189+
try:
190+
NEO4J_SERVICE = Neo4jService(auth=NEO4J_AUTH, image=request.param, n_cores=NEO4J_CORES, n_replicas=NEO4J_REPLICAS)
191+
NEO4J_SERVICE.start(timeout=300)
192+
except urllib.error.HTTPError as error:
193+
# pytest.skip(str(error))
194+
pytest.xfail(str(error) + " " + request.param)
188195
yield NEO4J_SERVICE
189196
if NEO4J_SERVICE is not None:
190197
NEO4J_SERVICE.stop(timeout=300)
191198
NEO4J_SERVICE = None
192199

193200

194-
@fixture(scope="session")
201+
@pytest.fixture(scope="session")
195202
def addresses(service):
196203
try:
197204
machines = service.cores()
@@ -209,7 +216,7 @@ def addresses(service):
209216
# return [machine.address for machine in machines]
210217

211218

212-
@fixture(scope="session")
219+
@pytest.fixture(scope="session")
213220
def address(addresses):
214221
try:
215222
return addresses[0]
@@ -225,7 +232,7 @@ def address(addresses):
225232
# return None
226233

227234

228-
@fixture(scope="session")
235+
@pytest.fixture(scope="session")
229236
def targets(addresses):
230237
return " ".join("{}:{}".format(address[0], address[1]) for address in addresses)
231238

@@ -235,7 +242,7 @@ def targets(addresses):
235242
# return " ".join("{}:{}".format(address[0], address[1]) for address in readonly_addresses)
236243

237244

238-
@fixture(scope="session")
245+
@pytest.fixture(scope="session")
239246
def target(address):
240247
return "{}:{}".format(address[0], address[1])
241248

@@ -248,17 +255,17 @@ def target(address):
248255
# return None
249256

250257

251-
@fixture(scope="session")
258+
@pytest.fixture(scope="session")
252259
def bolt_uri(service, target):
253260
return "bolt://" + target
254261

255262

256-
@fixture(scope="session")
263+
@pytest.fixture(scope="session")
257264
def neo4j_uri(service, target):
258265
return "neo4j://" + target
259266

260267

261-
@fixture(scope="session")
268+
@pytest.fixture(scope="session")
262269
def uri(bolt_uri):
263270
return bolt_uri
264271

@@ -271,47 +278,48 @@ def uri(bolt_uri):
271278
# return None
272279

273280

274-
@fixture(scope="session")
281+
@pytest.fixture(scope="session")
275282
def auth():
276283
return NEO4J_AUTH
277284

278285

279-
@fixture(scope="session")
286+
@pytest.fixture(scope="session")
280287
def bolt_driver(target, auth):
281288
try:
282289
driver = GraphDatabase.bolt_driver(target, auth=auth)
283290
try:
284291
yield driver
285292
finally:
286293
driver.close()
287-
except BoltHandshakeError as error:
288-
skip(error.args[0])
294+
except ServiceUnavailable as error:
295+
if isinstance(error.__cause__, BoltHandshakeError):
296+
pytest.skip(error.args[0])
289297

290298

291-
@fixture(scope="session")
299+
@pytest.fixture(scope="session")
292300
def neo4j_driver(target, auth):
293301
try:
294302
driver = GraphDatabase.neo4j_driver(target, auth=auth)
295303
except ServiceUnavailable as error:
296-
if error.args[0] == "Server does not support routing":
297-
skip(error.args[0])
304+
if isinstance(error.__cause__, BoltHandshakeError):
305+
pytest.skip(error.args[0])
306+
elif error.args[0] == "Server does not support routing":
307+
pytest.skip(error.args[0])
298308
else:
299309
raise
300-
except BoltHandshakeError as error:
301-
skip(error.args[0])
302310
else:
303311
try:
304312
yield driver
305313
finally:
306314
driver.close()
307315

308316

309-
@fixture(scope="session")
317+
@pytest.fixture(scope="session")
310318
def driver(neo4j_driver):
311319
return neo4j_driver
312320

313321

314-
@fixture()
322+
@pytest.fixture()
315323
def session(bolt_driver):
316324
session = bolt_driver.session()
317325
try:
@@ -320,14 +328,14 @@ def session(bolt_driver):
320328
session.close()
321329

322330

323-
@fixture()
331+
@pytest.fixture()
324332
def protocol_version(session):
325333
result = session.run("RETURN 1")
326334
yield session._connection.protocol_version
327335
result.consume()
328336

329337

330-
@fixture
338+
@pytest.fixture()
331339
def cypher_eval(bolt_driver):
332340

333341
def run_and_rollback(tx, cypher, **parameters):

0 commit comments

Comments
 (0)