Skip to content

Commit e5e7994

Browse files
r.inyakinMockird31
authored andcommitted
connection_pool: recover instances after a cluster restart
Treat any instance error as an unhealthy state instead of an unhandled exception. An instance which has not finished its bootstrap yet replies with an error to `box.info`, and the `Response` constructor raises it as a plain DatabaseError. `_get_new_state()` caught only a NetworkError, so the error escaped the background refresh loop and killed its thread. Close the socket if a handshake fails: `is_closed()` only checks the socket, so such a connection was reported as open and was never authenticated again. Closes #328
1 parent 1719e9c commit e5e7994

5 files changed

Lines changed: 85 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Set upper bound for version of setuptools (PR #342).
1515
- Reduce idle CPU usage in `ConnectionPool` while waiting for
1616
queued requests (PR #336).
17+
- Recover `ConnectionPool` instances after a cluster restart
18+
(PR #343).
1719

1820
## 1.2.0 - 2024-03-27
1921

tarantool/connection.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,7 @@ def connect(self):
11551155
raise exc
11561156
except Exception as exc:
11571157
self.connected = False
1158+
self.close()
11581159
raise NetworkError(exc) from exc
11591160

11601161
def _recv(self, to_read):
@@ -1271,7 +1272,8 @@ def _send_request_wo_reconnect(self, request, on_push=None, on_push_ctx=None):
12711272
12721273
:raise: :exc:`~AssertionError`,
12731274
:exc:`~tarantool.error.SchemaError`,
1274-
:exc:`~tarantool.error.NetworkError`
1275+
:exc:`~tarantool.error.NetworkError`,
1276+
:exc:`~tarantool.error.DatabaseError`
12751277
12761278
:meta private:
12771279
"""
@@ -1365,7 +1367,12 @@ def check(): # Check that connection is alive
13651367
attempt += 1
13661368
if self.transport == SSL_TRANSPORT:
13671369
self.wrap_socket_ssl()
1368-
self.handshake()
1370+
try:
1371+
self.handshake()
1372+
except Exception:
1373+
self.connected = False
1374+
self.close()
1375+
raise
13691376

13701377
def _send_request(self, request, on_push=None, on_push_ctx=None):
13711378
"""

tarantool/connection_pool.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
PoolTolopogyError,
2626
PoolTolopogyWarning,
2727
ConfigurationError,
28+
DatabaseError,
2829
NetworkError,
2930
warn
3031
)
@@ -575,7 +576,7 @@ def _get_new_state(self, unit):
575576

576577
try:
577578
resp = conn.call('box.info')
578-
except NetworkError as exc:
579+
except DatabaseError as exc:
579580
msg = (f"Failed to get box.info for {unit.get_address()}, "
580581
f"reason: {repr(exc)}")
581582
warn(msg, PoolTolopogyWarning)

test/suites/test_connection.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
# pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code
55

6+
import socket
67
import sys
78
import unittest
89
import decimal
@@ -12,6 +13,7 @@
1213

1314
import tarantool
1415
import tarantool.msgpack_ext.decimal as ext_decimal
16+
from tarantool.error import DatabaseError, NetworkError
1517

1618
from .lib.skip import skip_or_run_decimal_test, skip_or_run_varbinary_test
1719
from .lib.tarantool_server import TarantoolServer
@@ -171,6 +173,29 @@ def my_unpacker_factory(_):
171173
resp = self.con.eval("return {1, 2, 3}")
172174
self.assertIsInstance(resp[0], tuple)
173175

176+
def test_failed_connect_closes_socket(self):
177+
con = tarantool.Connection(self.srv.host, self.srv.args['primary'],
178+
user='nosuchuser', password='wrongpassword',
179+
connect_now=False)
180+
181+
with self.assertRaises(NetworkError):
182+
con.connect()
183+
184+
self.assertTrue(con.is_closed())
185+
186+
def test_failed_handshake_on_reconnect_closes_socket(self):
187+
self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'],
188+
user='test', password='test')
189+
self.assertFalse(self.con.is_closed())
190+
191+
self.con.user = 'nosuchuser'
192+
self.con._socket.shutdown(socket.SHUT_RDWR) # pylint: disable=protected-access
193+
194+
with self.assertRaises(DatabaseError):
195+
self.con.call('box.info')
196+
197+
self.assertTrue(self.con.is_closed())
198+
174199
def tearDown(self):
175200
if self.con:
176201
self.con.close()

test/suites/test_pool.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import warnings
1111

1212
import tarantool
13+
from tarantool.connection_pool import Status
1314
from tarantool.error import (
1415
ClusterConnectWarning,
1516
DatabaseError,
@@ -581,6 +582,52 @@ def test_16_is_closed(self):
581582

582583
self.assertEqual(self.pool.is_closed(), True)
583584

585+
def test_17_instance_bootstrap_error_does_not_kill_refresh(self):
586+
warnings.simplefilter('ignore', category=PoolTolopogyWarning)
587+
588+
self.set_cluster_ro([False, True, True, True, True])
589+
590+
self.pool = tarantool.ConnectionPool(
591+
addrs=self.addrs,
592+
user='test',
593+
password='test',
594+
refresh_delay=0.2)
595+
596+
self.pool.ping(mode=tarantool.Mode.RW)
597+
598+
unit = self.pool.pool[f"{self.addrs[0]['host']}:{self.addrs[0]['port']}"]
599+
600+
# Simulate an instance which is up, but has not finished its
601+
# bootstrap yet: box.info fails with a plain DatabaseError
602+
# instead of a NetworkError.
603+
resp = self.servers[0].admin(r"""
604+
rawset(_G, 'box_info_backup', box.info)
605+
box.info = function()
606+
box.error({code = 116,
607+
reason = "Instance bootstrap hasn't finished yet"})
608+
end
609+
return true
610+
""")
611+
assert_admin_success(resp)
612+
613+
def expect_instance_unhealthy_and_refresh_alive():
614+
self.assertTrue(unit.thread.is_alive(),
615+
'refresh thread died on a DatabaseError')
616+
self.assertEqual(unit.state.status, Status.UNHEALTHY)
617+
618+
self.retry(func=expect_instance_unhealthy_and_refresh_alive)
619+
620+
resp = self.servers[0].admin(r"""
621+
box.info = box_info_backup
622+
return true
623+
""")
624+
assert_admin_success(resp)
625+
626+
def expect_rw_request_succeed():
627+
self.pool.ping(mode=tarantool.Mode.RW)
628+
629+
self.retry(func=expect_rw_request_succeed)
630+
584631
def tearDown(self):
585632
if self.pool:
586633
self.pool.close()

0 commit comments

Comments
 (0)