Skip to content

Commit 85a6bbd

Browse files
committed
connection_pool: Add additional health check for pool instances
Allow `ConnectionPool` to exclude instances until an application running on top of Tarantool has completed its post-startup initialization. The built-in health check only confirms that Tarantool is reachable and `box.info.status == "running"`. The application may still be initializing its own components after Tarantool starts, so an additional health check prevents requests from being routed to the instance until the application reports that it is ready.
1 parent 5003558 commit 85a6bbd

3 files changed

Lines changed: 102 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88

99
### Added
10-
10+
11+
- Support additional health check for pool instances (PR #348).
12+
1113
### Changed
1214

1315
### Fixed

tarantool/connection_pool.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,8 @@ def __init__(self,
398398
connection_timeout=CONNECTION_TIMEOUT,
399399
strategy_class=RoundRobinStrategy,
400400
refresh_delay=POOL_REFRESH_DELAY,
401-
fetch_schema=True):
401+
fetch_schema=True,
402+
additional_health_check=None):
402403
"""
403404
:param addrs: List of dictionaries describing server addresses:
404405
@@ -476,6 +477,17 @@ def __init__(self,
476477
:param fetch_schema: Refer to
477478
:paramref:`~tarantool.Connection.params.fetch_schema`.
478479
480+
:param additional_health_check: An additional health check for each
481+
pool instance. It is called with the instance connection after
482+
the built-in checks succeed.
483+
Return :attr:`Status.UNHEALTHY` to exclude the
484+
instance from request routing until the next successful health
485+
check; return :attr:`Status.HEALTHY` to keep it available. The
486+
check is performed when the pool connects and during periodic
487+
state refreshes. It should be fast and must not raise exceptions;
488+
the pool does not handle callback errors.
489+
:type additional_health_check: :obj:`typing.Callable[[Connection], Status]`, optional
490+
479491
:raise: :exc:`~tarantool.error.ConfigurationError`,
480492
:class:`~tarantool.Connection` exceptions
481493
@@ -497,6 +509,8 @@ def __init__(self,
497509
new_addrs.append(new_addr)
498510
self.addrs = new_addrs
499511

512+
self.additional_health_check = additional_health_check
513+
500514
# Create connections
501515
self.pool = {}
502516
self.refresh_delay = refresh_delay
@@ -603,6 +617,12 @@ def _get_new_state(self, unit):
603617
warn(msg, PoolTopologyWarning)
604618
return InstanceState(Status.UNHEALTHY)
605619

620+
if self.additional_health_check is not None:
621+
status = self.additional_health_check(conn)
622+
623+
if status == Status.UNHEALTHY:
624+
return InstanceState(Status.UNHEALTHY)
625+
606626
return InstanceState(Status.HEALTHY, read_only)
607627

608628
def _refresh_state(self, key):

test/suites/test_pool.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,14 +594,24 @@ def test_17_instance_bootstrap_error_does_not_kill_refresh(self):
594594
warnings.simplefilter('ignore', category=PoolTolopogyWarning)
595595

596596
self.set_cluster_ro([False, True, True, True, True])
597+
callback_calls = 0
598+
target_port = self.addrs[0]['port']
599+
600+
def additional_health_check(conn):
601+
nonlocal callback_calls
602+
if conn.port == target_port:
603+
callback_calls += 1
604+
return Status.HEALTHY
597605

598606
self.pool = tarantool.ConnectionPool(
599607
addrs=self.addrs,
600608
user='test',
601609
password='test',
602-
refresh_delay=0.2)
610+
refresh_delay=0.2,
611+
additional_health_check=additional_health_check)
603612

604613
self.pool.ping(mode=tarantool.Mode.RW)
614+
callback_calls = 0
605615

606616
unit = self.pool.pool[f"{self.addrs[0]['host']}:{self.addrs[0]['port']}"]
607617

@@ -622,6 +632,7 @@ def expect_instance_unhealthy_and_refresh_alive():
622632
self.assertTrue(unit.thread.is_alive(),
623633
'refresh thread died on a DatabaseError')
624634
self.assertEqual(unit.state.status, Status.UNHEALTHY)
635+
self.assertEqual(callback_calls, 0)
625636

626637
self.retry(func=expect_instance_unhealthy_and_refresh_alive)
627638

@@ -636,6 +647,72 @@ def expect_rw_request_succeed():
636647

637648
self.retry(func=expect_rw_request_succeed)
638649

650+
def test_18_additional_health_check_excludes_and_recovers_instance(self):
651+
self.set_cluster_ro([False, False, True, False, False])
652+
target_addr = self.addrs[2]
653+
target_key = f"{target_addr['host']}:{target_addr['port']}"
654+
unhealthy = True
655+
656+
def additional_health_check(conn):
657+
if unhealthy and conn.port == target_addr['port']:
658+
return Status.UNHEALTHY
659+
return Status.HEALTHY
660+
661+
self.pool = tarantool.ConnectionPool(
662+
addrs=self.addrs,
663+
user='test',
664+
password='test',
665+
refresh_delay=0.2,
666+
additional_health_check=additional_health_check)
667+
668+
self.assertEqual(self.pool.pool[target_key].state.status,
669+
Status.UNHEALTHY)
670+
with self.assertRaises(PoolTopologyError):
671+
self.pool.ping(mode=tarantool.Mode.RO)
672+
673+
unhealthy = False
674+
675+
def expect_instance_recovered():
676+
self.assertEqual(self.pool.pool[target_key].state.status,
677+
Status.HEALTHY)
678+
self.pool.ping(mode=tarantool.Mode.RO)
679+
680+
self.retry(func=expect_instance_recovered)
681+
682+
def test_19_additional_health_check_not_called_before_running(self):
683+
warnings.simplefilter('ignore', category=PoolTopologyWarning)
684+
685+
self.set_cluster_ro([False, True, True, True, True])
686+
target_addr = self.addrs[0]
687+
callback_calls = 0
688+
689+
resp = self.servers[0].admin(r"""
690+
rawset(_G, 'box_info_backup', box.info)
691+
box.info = function()
692+
local info = box_info_backup()
693+
return {ro = info.ro, status = 'loading'}
694+
end
695+
return true
696+
""")
697+
assert_admin_success(resp)
698+
699+
def additional_health_check(conn):
700+
nonlocal callback_calls
701+
if conn.port == target_addr['port']:
702+
callback_calls += 1
703+
return Status.HEALTHY
704+
705+
self.pool = tarantool.ConnectionPool(
706+
addrs=self.addrs,
707+
user='test',
708+
password='test',
709+
additional_health_check=additional_health_check)
710+
711+
self.assertEqual(callback_calls, 0)
712+
self.assertEqual(
713+
self.pool.pool[f"{target_addr['host']}:{target_addr['port']}"].state.status,
714+
Status.UNHEALTHY)
715+
639716
def tearDown(self):
640717
if self.pool:
641718
self.pool.close()

0 commit comments

Comments
 (0)