Skip to content

Commit 23a8218

Browse files
committed
Improved connection close handling
1 parent 302868b commit 23a8218

File tree

10 files changed

+101
-52
lines changed

10 files changed

+101
-52
lines changed

neo4j/__init__.py

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,23 @@
4949

5050
try:
5151
from neobolt.exceptions import (
52+
ConnectionExpired,
5253
CypherError,
53-
TransientError,
54+
IncompleteCommitError,
5455
ServiceUnavailable,
56+
TransientError,
5557
)
5658
except ImportError:
5759
# We allow this to fail because this module can be imported implicitly
5860
# during setup. At that point, dependencies aren't available.
5961
pass
6062
else:
6163
__all__.extend([
64+
"ConnectionExpired",
6265
"CypherError",
63-
"TransientError",
66+
"IncompleteCommitError",
6467
"ServiceUnavailable",
68+
"TransientError",
6569
])
6670

6771

@@ -363,35 +367,31 @@ def _connect(self, access_mode=None):
363367
if access_mode is None:
364368
access_mode = self._default_access_mode
365369
if self._connection:
366-
self._disconnect(sync=True)
370+
self._connection.sync()
371+
self._disconnect()
367372
self._connection = self._acquirer(access_mode)
368373

369-
def _disconnect(self, sync):
370-
from neobolt.exceptions import ConnectionExpired, ServiceUnavailable
374+
def _disconnect(self):
371375
if self._connection:
372-
if sync:
373-
try:
374-
self._connection.sync()
375-
except (SessionError, ConnectionExpired, ServiceUnavailable):
376-
pass
377-
if self._connection:
378-
self._connection.in_use = False
379-
self._connection = None
376+
self._connection.in_use = False
377+
self._connection = None
380378

381379
def close(self):
382380
""" Close the session. This will release any borrowed resources,
383381
such as connections, and will roll back any outstanding transactions.
384382
"""
385-
from neobolt.exceptions import ConnectionExpired, CypherError, ServiceUnavailable
386-
try:
387-
if self.has_transaction():
388-
try:
389-
self.rollback_transaction()
390-
except (CypherError, TransactionError, SessionError, ConnectionExpired, ServiceUnavailable):
391-
pass
392-
finally:
393-
self._closed = True
394-
self._disconnect(sync=True)
383+
if self._connection:
384+
if self._transaction:
385+
self._connection.rollback()
386+
self._transaction = None
387+
try:
388+
self._connection.sync()
389+
except (ConnectionExpired, CypherError, TransactionError,
390+
ServiceUnavailable, SessionError):
391+
pass
392+
finally:
393+
self._disconnect()
394+
self._closed = True
395395

396396
def closed(self):
397397
""" Indicator for whether or not this session has been closed.
@@ -554,7 +554,7 @@ def detach(self, result, sync=True):
554554
if self._last_result is result:
555555
self._last_result = None
556556
if not self.has_transaction():
557-
self._disconnect(sync=False)
557+
self._disconnect()
558558

559559
result._session = None
560560
return count
@@ -620,8 +620,11 @@ def commit_transaction(self):
620620
metadata = {}
621621
try:
622622
self._connection.commit(on_success=metadata.update)
623+
self._connection.sync()
624+
except IncompleteCommitError:
625+
raise ServiceUnavailable("Connection closed during commit")
623626
finally:
624-
self._disconnect(sync=True)
627+
self._disconnect()
625628
self._transaction = None
626629
bookmark = metadata.get("bookmark")
627630
self._bookmarks_in = tuple([bookmark])
@@ -641,8 +644,9 @@ def rollback_transaction(self):
641644
metadata = {}
642645
try:
643646
cx.rollback(on_success=metadata.update)
647+
cx.sync()
644648
finally:
645-
self._disconnect(sync=True)
649+
self._disconnect()
646650
self._transaction = None
647651

648652
def _run_transaction(self, access_mode, unit_of_work, *args, **kwargs):

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
neobolt<2,>=1.7.4
1+
neobolt<2,>=1.7.6
22
neotime<2,>=1.7.1

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from neo4j.meta import package, version
3131

3232
install_requires = [
33-
"neobolt<2,>=1.7.4",
33+
"neobolt<2,>=1.7.6",
3434
"neotime<2,>=1.7.1",
3535
]
3636
classifiers = [

test/integration/test_session.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -396,24 +396,6 @@ def test_broken_transaction_should_not_break_session(self):
396396
with session.begin_transaction() as tx:
397397
tx.run("RETURN 1")
398398

399-
def test_last_run_statement_should_be_cleared_on_failure(self):
400-
if not self.at_least_server_version(3, 2):
401-
raise SkipTest("Statement reuse is not supported before server 3.2")
402-
403-
with self.driver.session() as session:
404-
tx = session.begin_transaction()
405-
tx.run("RETURN 1").consume()
406-
connection_1 = session._connection
407-
assert connection_1._last_run_statement == "RETURN 1"
408-
with self.assertRaises(CypherSyntaxError):
409-
result = tx.run("X")
410-
connection_2 = session._connection
411-
result.consume()
412-
# connection_2 = session._connection
413-
assert connection_2 is connection_1
414-
assert connection_2._last_run_statement is None
415-
tx.close()
416-
417399
def test_statement_object_not_supported(self):
418400
with self.driver.session() as session:
419401
with session.begin_transaction() as tx:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
!: BOLT 3
2+
!: AUTO HELLO
3+
!: AUTO RESET
4+
5+
C: BEGIN {}
6+
RUN "CREATE (n {name:'Bob'})" {} {}
7+
PULL_ALL
8+
S: SUCCESS {}
9+
SUCCESS {}
10+
SUCCESS {}
11+
C: COMMIT
12+
S: <EXIT>

test/stub/scripts/return_1_four_times.script

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ S: SUCCESS {"fields": ["x"]}
77
RECORD [1]
88
SUCCESS {}
99

10-
C: RUN "" {"x": 1}
10+
C: RUN "RETURN $x" {"x": 1}
1111
PULL_ALL
1212
S: SUCCESS {"fields": ["x"]}
1313
RECORD [1]
1414
SUCCESS {}
1515

16-
C: RUN "" {"x": 1}
16+
C: RUN "RETURN $x" {"x": 1}
1717
PULL_ALL
1818
S: SUCCESS {"fields": ["x"]}
1919
RECORD [1]
2020
SUCCESS {}
2121

22-
C: RUN "" {"x": 1}
22+
C: RUN "RETURN $x" {"x": 1}
2323
PULL_ALL
2424
S: SUCCESS {"fields": ["x"]}
2525
RECORD [1]

test/stub/scripts/return_1_in_tx_twice.script

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ C: RUN "BEGIN" {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
2222
S: SUCCESS {"fields": []}
2323
SUCCESS {}
2424

25-
C: RUN "" {}
25+
C: RUN "RETURN 1" {}
2626
PULL_ALL
2727
S: SUCCESS {"fields": ["1"]}
2828
RECORD [1]

test/stub/scripts/return_1_twice.script

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ S: SUCCESS {"fields": ["x"]}
77
RECORD [1]
88
SUCCESS {}
99

10-
C: RUN "" {"x": 1}
10+
C: RUN "RETURN $x" {"x": 1}
1111
PULL_ALL
1212
S: SUCCESS {"fields": ["x"]}
1313
RECORD [1]

test/stub/scripts/return_1_twice_in_tx.script

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ S: SUCCESS {"fields": ["x"]}
1212
RECORD [1]
1313
SUCCESS {}
1414

15-
C: RUN "" {"x": 1}
15+
C: RUN "RETURN $x" {"x": 1}
1616
PULL_ALL
1717
S: SUCCESS {"fields": ["x"]}
1818
RECORD [1]

test/stub/test_transactions.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
4+
# Copyright (c) 2002-2019 "Neo4j,"
5+
# Neo4j Sweden AB [http://neo4j.com]
6+
#
7+
# This file is part of Neo4j.
8+
#
9+
# Licensed under the Apache License, Version 2.0 (the "License");
10+
# you may not use this file except in compliance with the License.
11+
# You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing, software
16+
# distributed under the License is distributed on an "AS IS" BASIS,
17+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
# See the License for the specific language governing permissions and
19+
# limitations under the License.
20+
21+
22+
from neobolt.exceptions import ServiceUnavailable
23+
24+
from neo4j import GraphDatabase
25+
26+
from test.stub.tools import StubTestCase, StubCluster
27+
28+
29+
class TransactionTestCase(StubTestCase):
30+
31+
@staticmethod
32+
def create_bob(tx):
33+
tx.run("CREATE (n {name:'Bob'})").data()
34+
35+
def test_connection_error_on_explicit_commit(self):
36+
with StubCluster({9001: "connection_error_on_commit.script"}):
37+
uri = "bolt://127.0.0.1:9001"
38+
with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False, max_retry_time=0) as driver:
39+
with driver.session() as session:
40+
tx = session.begin_transaction()
41+
tx.run("CREATE (n {name:'Bob'})").data()
42+
with self.assertRaises(ServiceUnavailable):
43+
tx.commit()
44+
45+
def test_connection_error_on_commit(self):
46+
with StubCluster({9001: "connection_error_on_commit.script"}):
47+
uri = "bolt://127.0.0.1:9001"
48+
with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False, max_retry_time=0) as driver:
49+
with driver.session() as session:
50+
with self.assertRaises(ServiceUnavailable):
51+
session.write_transaction(self.create_bob)

0 commit comments

Comments
 (0)