Skip to content

Commit ded674c

Browse files
committed
Don't attempt to reset closed connections.
Also fix AttributeError on connection clean-up
1 parent 7eb49eb commit ded674c

File tree

8 files changed

+124
-20
lines changed

8 files changed

+124
-20
lines changed

neo4j/io/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,9 +726,9 @@ def release(self, *connections):
726726
"""
727727
with self.lock:
728728
for connection in connections:
729-
if not (connection.is_reset
730-
or connection.defunct()
731-
or connection.closed()):
729+
if not (connection.defunct()
730+
or connection.closed()
731+
or connection.is_reset):
732732
try:
733733
connection.reset()
734734
except (Neo4jError, DriverError, BoltError) as e:

neo4j/io/_bolt3.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,12 @@ def _on_server_state_change(self, old_state, new_state):
124124

125125
@property
126126
def is_reset(self):
127-
if self.responses:
128-
# We can't be sure of the server's state as there are still pending
129-
# responses. Unless the last message we sent was RESET. In that case
130-
# the server state will always be READY when we're done.
131-
return self.responses[-1].message == "reset"
127+
# We can't be sure of the server's state if there are still pending
128+
# responses. Unless the last message we sent was RESET. In that case
129+
# the server state will always be READY when we're done.
130+
if (self.responses and self.responses[-1]
131+
and self.responses[-1].message == "reset"):
132+
return True
132133
return self._server_state_manager.state == ServerStates.READY
133134

134135
@property

neo4j/io/_bolt4.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,12 @@ def _on_server_state_change(self, old_state, new_state):
8282

8383
@property
8484
def is_reset(self):
85-
if self.responses:
86-
# We can't be sure of the server's state as there are still pending
87-
# responses. Unless the last message we sent was RESET. In that case
88-
# the server state will always be READY when we're done.
89-
return self.responses[-1].message == "reset"
85+
# We can't be sure of the server's state if there are still pending
86+
# responses. Unless the last message we sent was RESET. In that case
87+
# the server state will always be READY when we're done.
88+
if (self.responses and self.responses[-1]
89+
and self.responses[-1].message == "reset"):
90+
return True
9091
return self._server_state_manager.state == ServerStates.READY
9192

9293
@property

neo4j/work/simple.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def __init__(self, pool, session_config):
9191
def __del__(self):
9292
try:
9393
self.close()
94-
except OSError:
94+
except (OSError, ServiceUnavailable, SessionExpired):
9595
pass
9696

9797
def __enter__(self):

neo4j/work/transaction.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ def rollback(self):
172172

173173
metadata = {}
174174
try:
175-
if not self._connection.is_reset:
175+
if not (self._connection.defunct()
176+
or self._connection.closed()
177+
or self._connection.is_reset):
176178
# DISCARD pending records then do a rollback.
177179
self._consume_results()
178180
self._connection.rollback(on_success=metadata.update)

tests/unit/io/test_neo4j_pool.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,37 @@ def break_connection():
161161
assert cx2.addr == cx1.addr
162162
assert cx1 not in pool.connections[cx1.addr]
163163
assert cx2 in pool.connections[cx2.addr]
164+
165+
166+
def test_release_resets_connections(opener):
167+
pool = Neo4jPool(opener, PoolConfig(), WorkspaceConfig(), ROUTER_ADDRESS)
168+
cx1 = pool.acquire(READ_ACCESS, 30, "test_db", None)
169+
cx1.is_reset_mock.return_value = False
170+
cx1.is_reset_mock.reset_mock()
171+
pool.release(cx1)
172+
cx1.is_reset_mock.assert_called_once()
173+
cx1.reset.assert_called_once()
174+
175+
176+
def test_release_does_not_resets_closed_connections(opener):
177+
pool = Neo4jPool(opener, PoolConfig(), WorkspaceConfig(), ROUTER_ADDRESS)
178+
cx1 = pool.acquire(READ_ACCESS, 30, "test_db", None)
179+
cx1.closed.return_value = True
180+
cx1.closed.reset_mock()
181+
cx1.is_reset_mock.reset_mock()
182+
pool.release(cx1)
183+
cx1.closed.assert_called_once()
184+
cx1.is_reset_mock.asset_not_called()
185+
cx1.reset.asset_not_called()
186+
187+
188+
def test_release_does_not_resets_defunct_connections(opener):
189+
pool = Neo4jPool(opener, PoolConfig(), WorkspaceConfig(), ROUTER_ADDRESS)
190+
cx1 = pool.acquire(READ_ACCESS, 30, "test_db", None)
191+
cx1.defunct.return_value = True
192+
cx1.defunct.reset_mock()
193+
cx1.is_reset_mock.reset_mock()
194+
pool.release(cx1)
195+
cx1.defunct.assert_called_once()
196+
cx1.is_reset_mock.asset_not_called()
197+
cx1.reset.asset_not_called()

tests/unit/work/_fake_connection.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class FakeConnection(mock.NonCallableMagicMock):
3333

3434
def __init__(self, *args, **kwargs):
3535
super().__init__(*args, **kwargs)
36-
self.attach_mock(mock.PropertyMock(return_value=True), "is_reset")
36+
self.attach_mock(mock.Mock(return_value=True), "is_reset_mock")
3737
self.attach_mock(mock.Mock(return_value=False), "defunct")
3838
self.attach_mock(mock.Mock(return_value=False), "stale")
3939
self.attach_mock(mock.Mock(return_value=False), "closed")
@@ -43,6 +43,13 @@ def close_side_effect():
4343

4444
self.attach_mock(mock.Mock(side_effect=close_side_effect), "close")
4545

46+
@property
47+
def is_reset(self):
48+
if self.closed.return_value or self.defunct.return_value:
49+
raise AssertionError("is_reset should not be called on a closed or "
50+
"defunct connection.")
51+
return self.is_reset_mock()
52+
4653
def fetch_message(self, *args, **kwargs):
4754
if self.callbacks:
4855
cb = self.callbacks.pop(0)
@@ -78,13 +85,13 @@ def callback():
7885
else:
7986
cb()
8087
self.callbacks.append(callback)
81-
return parent.__getattr__(name)(*args, **kwargs)
8288

8389
return func
8490

91+
method_mock = parent.__getattr__(name)
8592
if name in ("run", "commit", "pull", "rollback", "discard"):
86-
return build_message_handler(name)
87-
return parent.__getattr__(name)
93+
method_mock.side_effect = build_message_handler(name)
94+
return method_mock
8895

8996

9097
@pytest.fixture

tests/unit/work/test_transaction.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
# limitations under the License.
2020

2121
from uuid import uuid4
22-
from unittest.mock import MagicMock
22+
from unittest.mock import (
23+
MagicMock,
24+
NonCallableMagicMock,
25+
)
2326

2427
import pytest
2528

@@ -129,3 +132,59 @@ def test_transaction_run_takes_no_query_object(fake_connection):
129132
tx = Transaction(fake_connection, 2, on_closed, on_error)
130133
with pytest.raises(ValueError):
131134
tx.run(Query("RETURN 1"))
135+
136+
137+
def test_transaction_rollbacks_on_open_connections(fake_connection):
138+
tx = Transaction(fake_connection, 2,
139+
lambda *args, **kwargs: None,
140+
lambda *args, **kwargs: None)
141+
with tx as tx_:
142+
fake_connection.is_reset_mock.return_value = False
143+
fake_connection.is_reset_mock.reset_mock()
144+
tx_.rollback()
145+
fake_connection.is_reset_mock.assert_called_once()
146+
fake_connection.reset.assert_not_called()
147+
fake_connection.rollback.assert_called_once()
148+
149+
150+
def test_transaction_no_rollback_on_reset_connections(fake_connection):
151+
tx = Transaction(fake_connection, 2,
152+
lambda *args, **kwargs: None,
153+
lambda *args, **kwargs: None)
154+
with tx as tx_:
155+
fake_connection.is_reset_mock.return_value = True
156+
fake_connection.is_reset_mock.reset_mock()
157+
tx_.rollback()
158+
fake_connection.is_reset_mock.assert_called_once()
159+
fake_connection.reset.asset_not_called()
160+
fake_connection.rollback.asset_not_called()
161+
162+
163+
def test_transaction_no_rollback_on_closed_connections(fake_connection):
164+
tx = Transaction(fake_connection, 2,
165+
lambda *args, **kwargs: None,
166+
lambda *args, **kwargs: None)
167+
with tx as tx_:
168+
fake_connection.closed.return_value = True
169+
fake_connection.closed.reset_mock()
170+
fake_connection.is_reset_mock.reset_mock()
171+
tx_.rollback()
172+
fake_connection.closed.assert_called_once()
173+
fake_connection.is_reset_mock.asset_not_called()
174+
fake_connection.reset.asset_not_called()
175+
fake_connection.rollback.asset_not_called()
176+
177+
178+
def test_transaction_no_rollback_on_defunct_connections(fake_connection):
179+
tx = Transaction(fake_connection, 2,
180+
lambda *args, **kwargs: None,
181+
lambda *args, **kwargs: None)
182+
with tx as tx_:
183+
fake_connection.defunct.return_value = True
184+
fake_connection.defunct.reset_mock()
185+
fake_connection.is_reset_mock.reset_mock()
186+
tx_.rollback()
187+
fake_connection.defunct.assert_called_once()
188+
fake_connection.is_reset_mock.asset_not_called()
189+
fake_connection.reset.asset_not_called()
190+
fake_connection.rollback.asset_not_called()

0 commit comments

Comments
 (0)