mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-25 14:53:37 +00:00
Quorum based failover (#2668)
To enable quorum commit: ```diff $ patronictl.py edit-config --- +++ @@ -5,3 +5,4 @@ use_pg_rewind: true retry_timeout: 10 ttl: 30 +synchronous_mode: quorum Apply these changes? [y/N]: y Configuration changed ``` By default Patroni will use `ANY 1(list,of,stanbys)` in `synchronous_standby_names`. That is, only one node out of listed replicas will be used for quorum. If you want to increase the number of quorum nodes it is possible to do it with: ```diff $ patronictl edit-config --- +++ @@ -6,3 +6,4 @@ retry_timeout: 10 synchronous_mode: quorum ttl: 30 +synchronous_node_count: 2 Apply these changes? [y/N]: y Configuration changed ``` Good old `synchronous_mode: on` is still supported. Close https://github.com/patroni/patroni/issues/664 Close https://github.com/zalando/patroni/pull/672
This commit is contained in:
+5
-2
@@ -205,7 +205,6 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
def test_do_GET(self):
|
||||
MockPostgresql.pending_restart_reason = {'max_connections': get_param_diff('200', '100')}
|
||||
MockPatroni.dcs.cluster.status.last_lsn = 20
|
||||
MockPatroni.dcs.cluster.sync.members = [MockPostgresql.name]
|
||||
with patch.object(global_config.__class__, 'is_synchronous_mode', PropertyMock(return_value=True)):
|
||||
MockRestApiServer(RestApiHandler, 'GET /replica')
|
||||
MockRestApiServer(RestApiHandler, 'GET /replica?lag=1M')
|
||||
@@ -223,12 +222,16 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
Mock(return_value={'role': 'replica', 'sync_standby': True})):
|
||||
MockRestApiServer(RestApiHandler, 'GET /synchronous')
|
||||
MockRestApiServer(RestApiHandler, 'GET /read-only-sync')
|
||||
with patch.object(RestApiHandler, 'get_postgresql_status',
|
||||
Mock(return_value={'role': 'replica', 'quorum_standby': True})):
|
||||
MockRestApiServer(RestApiHandler, 'GET /quorum')
|
||||
MockRestApiServer(RestApiHandler, 'GET /read-only-quorum')
|
||||
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'replica'})):
|
||||
MockPatroni.dcs.cluster.sync.members = []
|
||||
MockRestApiServer(RestApiHandler, 'GET /asynchronous')
|
||||
with patch.object(MockHa, 'is_leader', Mock(return_value=True)):
|
||||
MockRestApiServer(RestApiHandler, 'GET /replica')
|
||||
MockRestApiServer(RestApiHandler, 'GET /read-only-sync')
|
||||
MockRestApiServer(RestApiHandler, 'GET /read-only-quorum')
|
||||
with patch.object(global_config.__class__, 'is_standby_cluster', Mock(return_value=True)):
|
||||
MockRestApiServer(RestApiHandler, 'GET /standby_leader')
|
||||
MockPatroni.dcs.cluster = None
|
||||
|
||||
+1
-1
@@ -344,7 +344,7 @@ class TestEtcd(unittest.TestCase):
|
||||
self.assertTrue(self.etcd.watch(None, 1))
|
||||
|
||||
def test_sync_state(self):
|
||||
self.assertIsNone(self.etcd.write_sync_state('leader', None))
|
||||
self.assertIsNone(self.etcd.write_sync_state('leader', None, 0))
|
||||
self.assertFalse(self.etcd.delete_sync_state())
|
||||
|
||||
def test_set_history_value(self):
|
||||
|
||||
+136
-17
@@ -18,6 +18,7 @@ from patroni.postgresql.config import ConfigHandler
|
||||
from patroni.postgresql.postmaster import PostmasterProcess
|
||||
from patroni.postgresql.rewind import Rewind
|
||||
from patroni.postgresql.slots import SlotsHandler
|
||||
from patroni.postgresql.sync import _SyncState
|
||||
from patroni.utils import tzutc
|
||||
from patroni.watchdog import Watchdog
|
||||
|
||||
@@ -63,7 +64,7 @@ def get_cluster_initialized_without_leader(leader=False, failover=None, sync=Non
|
||||
'tags': {'clonefrom': True},
|
||||
'scheduled_restart': {'schedule': "2100-01-01 10:53:07.560445+00:00",
|
||||
'postgres_version': '99.0.0'}})
|
||||
syncstate = SyncState(0 if sync else None, sync and sync[0], sync and sync[1])
|
||||
syncstate = SyncState(0 if sync else None, sync and sync[0], sync and sync[1], 0)
|
||||
failsafe = {m.name: m.api_url for m in (m1, m2)} if failsafe else None
|
||||
return get_cluster(SYSID, leader, [m1, m2], failover, syncstate, cluster_config, failsafe)
|
||||
|
||||
@@ -207,6 +208,7 @@ class TestHa(PostgresInit):
|
||||
@patch('patroni.dcs.dcs_modules', Mock(return_value=['patroni.dcs.etcd']))
|
||||
@patch.object(etcd.Client, 'read', etcd_read)
|
||||
@patch.object(AbstractEtcdClientWithFailover, '_get_machines_list', Mock(return_value=['http://remotehost:2379']))
|
||||
@patch.object(Config, '_load_cache', Mock())
|
||||
def setUp(self):
|
||||
super(TestHa, self).setUp()
|
||||
self.p.set_state('running')
|
||||
@@ -1309,7 +1311,7 @@ class TestHa(PostgresInit):
|
||||
self.ha.demote('immediate')
|
||||
follow.assert_called_once_with(None)
|
||||
|
||||
def test_process_sync_replication(self):
|
||||
def test__process_multisync_replication(self):
|
||||
self.ha.has_lock = true
|
||||
mock_set_sync = self.p.sync_handler.set_synchronous_standby_names = Mock()
|
||||
mock_cfg_set_sync = self.p.config.set_synchronous_standby_names = Mock()
|
||||
@@ -1339,8 +1341,9 @@ class TestHa(PostgresInit):
|
||||
self.ha.is_synchronous_mode = true
|
||||
|
||||
# Test sync standby not touched when picking the same node
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(['other']),
|
||||
CaseInsensitiveSet(['other'])))
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 1, 1,
|
||||
CaseInsensitiveSet(['other']),
|
||||
CaseInsensitiveSet(['other'])))
|
||||
self.ha.cluster = get_cluster_initialized_with_leader(sync=('leader', 'other'))
|
||||
self.ha.run_cycle()
|
||||
mock_set_sync.assert_not_called()
|
||||
@@ -1349,15 +1352,17 @@ class TestHa(PostgresInit):
|
||||
mock_cfg_set_sync.reset_mock()
|
||||
|
||||
# Test sync standby is replaced when switching standbys
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(['other2']), CaseInsensitiveSet()))
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 0, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet(['other2'])))
|
||||
self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
|
||||
self.ha.run_cycle()
|
||||
mock_set_sync.assert_called_once_with(CaseInsensitiveSet(['other2']))
|
||||
mock_cfg_set_sync.assert_not_called()
|
||||
|
||||
# Test sync standby is replaced when new standby is joined
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(['other2', 'other3']),
|
||||
CaseInsensitiveSet(['other2'])))
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 1, 1,
|
||||
CaseInsensitiveSet(['other2']),
|
||||
CaseInsensitiveSet(['other2', 'other3'])))
|
||||
self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
|
||||
self.ha.run_cycle()
|
||||
self.assertEqual(mock_set_sync.call_args_list[0][0], (CaseInsensitiveSet(['other2']),))
|
||||
@@ -1378,8 +1383,9 @@ class TestHa(PostgresInit):
|
||||
self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
|
||||
self.ha.dcs.get_cluster = Mock(return_value=get_cluster_initialized_with_leader(sync=('leader', 'other')))
|
||||
# self.ha.cluster = get_cluster_initialized_with_leader(sync=('leader', 'other'))
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(['other2']),
|
||||
CaseInsensitiveSet(['other2'])))
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 1, 1,
|
||||
CaseInsensitiveSet(['other2']),
|
||||
CaseInsensitiveSet(['other2'])))
|
||||
self.ha.run_cycle()
|
||||
self.assertEqual(self.ha.dcs.write_sync_state.call_count, 2)
|
||||
|
||||
@@ -1402,9 +1408,10 @@ class TestHa(PostgresInit):
|
||||
# Test sync set to '*' when synchronous_mode_strict is enabled
|
||||
mock_set_sync.reset_mock()
|
||||
mock_cfg_set_sync.reset_mock()
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(), CaseInsensitiveSet()))
|
||||
self.ha.cluster.config.data['synchronous_mode_strict'] = True
|
||||
self.ha.run_cycle()
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 0, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet()))
|
||||
with patch.object(global_config.__class__, 'is_synchronous_mode_strict', PropertyMock(return_value=True)):
|
||||
self.ha.run_cycle()
|
||||
mock_set_sync.assert_called_once_with(CaseInsensitiveSet('*'))
|
||||
mock_cfg_set_sync.assert_not_called()
|
||||
|
||||
@@ -1432,8 +1439,8 @@ class TestHa(PostgresInit):
|
||||
|
||||
# When we just became primary nobody is sync
|
||||
self.assertEqual(self.ha.enforce_primary_role('msg', 'promote msg'), 'promote msg')
|
||||
mock_set_sync.assert_called_once_with(CaseInsensitiveSet())
|
||||
mock_write_sync.assert_called_once_with('leader', None, version=0)
|
||||
mock_set_sync.assert_called_once_with(CaseInsensitiveSet(), 0)
|
||||
mock_write_sync.assert_called_once_with('leader', None, 0, version=0)
|
||||
|
||||
mock_set_sync.reset_mock()
|
||||
|
||||
@@ -1471,7 +1478,7 @@ class TestHa(PostgresInit):
|
||||
mock_acquire.assert_called_once()
|
||||
mock_follow.assert_not_called()
|
||||
mock_promote.assert_called_once()
|
||||
mock_write_sync.assert_called_once_with('other', None, version=0)
|
||||
mock_write_sync.assert_called_once_with('other', None, 0, version=0)
|
||||
|
||||
def test_disable_sync_when_restarting(self):
|
||||
self.ha.is_synchronous_mode = true
|
||||
@@ -1513,7 +1520,8 @@ class TestHa(PostgresInit):
|
||||
self.ha.is_synchronous_mode = true
|
||||
self.ha.has_lock = true
|
||||
self.p.name = 'leader'
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(), CaseInsensitiveSet()))
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 0, 0,
|
||||
CaseInsensitiveSet(), CaseInsensitiveSet()))
|
||||
self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
|
||||
with patch('patroni.ha.logger.info') as mock_logger:
|
||||
self.ha.run_cycle()
|
||||
@@ -1529,7 +1537,8 @@ class TestHa(PostgresInit):
|
||||
self.ha.has_lock = true
|
||||
self.p.name = 'leader'
|
||||
self.ha.cluster = get_cluster_initialized_without_leader(sync=('leader', 'a'))
|
||||
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet('a'), CaseInsensitiveSet()))
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('priority', 0, 0,
|
||||
CaseInsensitiveSet(), CaseInsensitiveSet('a')))
|
||||
self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
|
||||
mock_set_sync = self.p.sync_handler.set_synchronous_standby_names = Mock()
|
||||
with patch('patroni.ha.logger.warning') as mock_logger:
|
||||
@@ -1699,3 +1708,113 @@ class TestHa(PostgresInit):
|
||||
mock_logger.assert_called()
|
||||
self.assertTrue(mock_logger.call_args[0][0].startswith('Request to %s coordinator leader'))
|
||||
self.assertEqual(mock_logger.call_args[0][1], 'Citus')
|
||||
|
||||
@patch.object(global_config.__class__, 'is_synchronous_mode', PropertyMock(return_value=True))
|
||||
@patch.object(global_config.__class__, 'is_quorum_commit_mode', PropertyMock(return_value=True))
|
||||
def test_process_sync_replication_prepromote(self):
|
||||
self.p._major_version = 90500
|
||||
self.ha.cluster = get_cluster_initialized_without_leader(sync=('other', self.p.name + ',foo'))
|
||||
self.p.is_primary = false
|
||||
self.p.set_role('replica')
|
||||
mock_write_sync = self.ha.dcs.write_sync_state = Mock(return_value=None)
|
||||
# Postgres 9.5, write_sync_state to DCS failed
|
||||
self.assertEqual(self.ha.run_cycle(),
|
||||
'Postponing promotion because synchronous replication state was updated by somebody else')
|
||||
self.assertEqual(self.ha.dcs.write_sync_state.call_count, 1)
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][0], (self.p.name, None, 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][1], {'version': 0})
|
||||
|
||||
mock_set_sync = self.p.config.set_synchronous_standby_names = Mock()
|
||||
mock_write_sync = self.ha.dcs.write_sync_state = Mock(return_value=True)
|
||||
# Postgres 9.5, our name is written to leader of the /sync key, while voters list and ssn is empty
|
||||
self.assertEqual(self.ha.run_cycle(), 'promoted self to leader by acquiring session lock')
|
||||
self.assertEqual(self.ha.dcs.write_sync_state.call_count, 1)
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][0], (self.p.name, None, 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][1], {'version': 0})
|
||||
self.assertEqual(mock_set_sync.call_count, 1)
|
||||
self.assertEqual(mock_set_sync.call_args_list[0][0], (None,))
|
||||
|
||||
self.p._major_version = 90600
|
||||
mock_set_sync.reset_mock()
|
||||
mock_write_sync.reset_mock()
|
||||
self.p.set_role('replica')
|
||||
# Postgres 9.6, with quorum commit we avoid updating /sync key and put some nodes to ssn
|
||||
self.assertEqual(self.ha.run_cycle(), 'promoted self to leader by acquiring session lock')
|
||||
self.assertEqual(mock_write_sync.call_count, 0)
|
||||
self.assertEqual(mock_set_sync.call_count, 1)
|
||||
self.assertEqual(mock_set_sync.call_args_list[0][0], ('2 (foo,other)',))
|
||||
|
||||
self.p._major_version = 150000
|
||||
mock_set_sync.reset_mock()
|
||||
self.p.set_role('replica')
|
||||
self.p.name = 'nonsync'
|
||||
self.ha.fetch_node_status = get_node_status()
|
||||
# Postgres 15, with quorum commit. Non-sync node promoted we avoid updating /sync key and put some nodes to ssn
|
||||
self.assertEqual(self.ha.run_cycle(), 'promoted self to leader by acquiring session lock')
|
||||
self.assertEqual(mock_write_sync.call_count, 0)
|
||||
self.assertEqual(mock_set_sync.call_count, 1)
|
||||
self.assertEqual(mock_set_sync.call_args_list[0][0], ('ANY 3 (foo,other,postgresql0)',))
|
||||
|
||||
@patch.object(global_config.__class__, 'is_synchronous_mode', PropertyMock(return_value=True))
|
||||
@patch.object(global_config.__class__, 'is_quorum_commit_mode', PropertyMock(return_value=True))
|
||||
def test__process_quorum_replication(self):
|
||||
self.p._major_version = 150000
|
||||
self.ha.has_lock = true
|
||||
mock_set_sync = self.p.config.set_synchronous_standby_names = Mock()
|
||||
self.p.name = 'leader'
|
||||
|
||||
mock_write_sync = self.ha.dcs.write_sync_state = Mock(return_value=None)
|
||||
# Test /sync key is attempted to set and failed when missing or invalid
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('quorum', 1, 1, CaseInsensitiveSet(['other']),
|
||||
CaseInsensitiveSet(['other'])))
|
||||
self.ha.run_cycle()
|
||||
self.assertEqual(mock_write_sync.call_count, 1)
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][0], (self.p.name, None, 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][1], {'version': None})
|
||||
self.assertEqual(mock_set_sync.call_count, 0)
|
||||
|
||||
self.ha._promote_timestamp = 1
|
||||
mock_write_sync = self.ha.dcs.write_sync_state = Mock(side_effect=[SyncState(None, self.p.name, None, 0), None])
|
||||
# Test /sync key is attempted to set and succeed when missing or invalid
|
||||
with patch.object(SyncState, 'is_empty', Mock(side_effect=[True, False])):
|
||||
self.ha.run_cycle()
|
||||
self.assertEqual(mock_write_sync.call_count, 2)
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][0], (self.p.name, None, 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][1], {'version': None})
|
||||
self.assertEqual(mock_write_sync.call_args_list[1][0], (self.p.name, CaseInsensitiveSet(['other']), 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[1][1], {'version': None})
|
||||
self.assertEqual(mock_set_sync.call_count, 0)
|
||||
|
||||
self.p.sync_handler.current_state = Mock(side_effect=[_SyncState('quorum', 1, 0, CaseInsensitiveSet(['foo']),
|
||||
CaseInsensitiveSet(['other'])),
|
||||
_SyncState('quorum', 1, 1, CaseInsensitiveSet(['foo']),
|
||||
CaseInsensitiveSet(['foo']))])
|
||||
mock_write_sync = self.ha.dcs.write_sync_state = Mock(return_value=SyncState(1, 'leader', 'foo', 0))
|
||||
self.ha.cluster = get_cluster_initialized_with_leader(sync=('leader', 'foo'))
|
||||
# Test the sync node is removed from voters, added to ssn
|
||||
with patch.object(Postgresql, 'synchronous_standby_names', Mock(return_value='other')), \
|
||||
patch('time.sleep', Mock()):
|
||||
self.ha.run_cycle()
|
||||
self.assertEqual(mock_write_sync.call_count, 1)
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][0], (self.p.name, CaseInsensitiveSet(), 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][1], {'version': 0})
|
||||
self.assertEqual(mock_set_sync.call_count, 1)
|
||||
self.assertEqual(mock_set_sync.call_args_list[0][0], ('ANY 1 (other)',))
|
||||
|
||||
# Test ANY 1 (*) when synchronous_mode_strict and no nodes available
|
||||
self.p.sync_handler.current_state = Mock(return_value=_SyncState('quorum', 1, 0,
|
||||
CaseInsensitiveSet(['other', 'foo']),
|
||||
CaseInsensitiveSet()))
|
||||
mock_write_sync.reset_mock()
|
||||
mock_set_sync.reset_mock()
|
||||
with patch.object(global_config.__class__, 'is_synchronous_mode_strict', PropertyMock(return_value=True)):
|
||||
self.ha.run_cycle()
|
||||
self.assertEqual(mock_write_sync.call_count, 1)
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][0], (self.p.name, CaseInsensitiveSet(), 0))
|
||||
self.assertEqual(mock_write_sync.call_args_list[0][1], {'version': 0})
|
||||
self.assertEqual(mock_set_sync.call_count, 1)
|
||||
self.assertEqual(mock_set_sync.call_args_list[0][0], ('ANY 1 (*)',))
|
||||
|
||||
# Test that _process_quorum_replication doesn't take longer than loop_wait
|
||||
with patch('time.time', Mock(side_effect=[30, 60, 90, 120])):
|
||||
self.ha.process_sync_replication()
|
||||
|
||||
@@ -439,7 +439,7 @@ class TestKubernetesEndpoints(BaseTestKubernetes):
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_endpoints', mock_namespaced_kind, create=True)
|
||||
def test_write_sync_state(self):
|
||||
self.assertIsNotNone(self.k.write_sync_state('a', ['b'], 1))
|
||||
self.assertIsNotNone(self.k.write_sync_state('a', ['b'], 0, 1))
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_pod', mock_namespaced_kind, create=True)
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', mock_namespaced_kind, create=True)
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
import unittest
|
||||
|
||||
from typing import List, Set, Tuple
|
||||
|
||||
from patroni.quorum import QuorumStateResolver, QuorumError
|
||||
|
||||
|
||||
class QuorumTest(unittest.TestCase):
|
||||
|
||||
def check_state_transitions(self, leader: str, quorum: int, voters: Set[str], numsync: int, sync: Set[str],
|
||||
numsync_confirmed: int, active: Set[str], sync_wanted: int, leader_wanted: str,
|
||||
expected: List[Tuple[str, str, int, Set[str]]]) -> None:
|
||||
kwargs = {
|
||||
'leader': leader, 'quorum': quorum, 'voters': voters,
|
||||
'numsync': numsync, 'sync': sync, 'numsync_confirmed': numsync_confirmed,
|
||||
'active': active, 'sync_wanted': sync_wanted, 'leader_wanted': leader_wanted
|
||||
}
|
||||
result = list(QuorumStateResolver(**kwargs))
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
# also check interrupted transitions
|
||||
if len(result) > 0 and result[0][0] != 'restart' and kwargs['leader'] == result[0][1]:
|
||||
if result[0][0] == 'sync':
|
||||
kwargs.update(numsync=result[0][2], sync=result[0][3])
|
||||
else:
|
||||
kwargs.update(leader=result[0][1], quorum=result[0][2], voters=result[0][3])
|
||||
kwargs['expected'] = expected[1:]
|
||||
self.check_state_transitions(**kwargs)
|
||||
|
||||
def test_1111(self):
|
||||
leader = 'a'
|
||||
|
||||
# Add node
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set(),
|
||||
numsync=0, sync=set(), numsync_confirmed=0, active=set('b'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 1, set('b')),
|
||||
('restart', leader, 0, set()),
|
||||
])
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set(),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('b'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('b'))
|
||||
])
|
||||
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set(),
|
||||
numsync=0, sync=set(), numsync_confirmed=0, active=set('bcde'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bcde')),
|
||||
('restart', leader, 0, set()),
|
||||
])
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set(),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=1, active=set('bcde'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 3, set('bcde')),
|
||||
])
|
||||
|
||||
def test_1222(self):
|
||||
"""2 node cluster"""
|
||||
leader = 'a'
|
||||
|
||||
# Active set matches state
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('b'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[])
|
||||
|
||||
# Add node by increasing quorum
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('BC'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 1, set('bC')),
|
||||
('sync', leader, 1, set('bC')),
|
||||
])
|
||||
|
||||
# Add node by increasing sync
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('bc'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bc')),
|
||||
('quorum', leader, 1, set('bc')),
|
||||
])
|
||||
# Reduce quorum after added node caught up
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=2, active=set('bc'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('bc')),
|
||||
])
|
||||
|
||||
# Add multiple nodes by increasing both sync and quorum
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('BCdE'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bC')),
|
||||
('quorum', leader, 3, set('bCdE')),
|
||||
('sync', leader, 2, set('bCdE')),
|
||||
])
|
||||
# Reduce quorum after added nodes caught up
|
||||
self.check_state_transitions(leader=leader, quorum=3, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=3, active=set('bcde'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 2, set('bcde')),
|
||||
])
|
||||
|
||||
# Primary is alone
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=0, active=set(),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set()),
|
||||
('sync', leader, 0, set()),
|
||||
])
|
||||
|
||||
# Swap out sync replica
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=0, active=set('c'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set()),
|
||||
('sync', leader, 1, set('c')),
|
||||
('restart', leader, 0, set()),
|
||||
])
|
||||
# Update quorum when added node caught up
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set(),
|
||||
numsync=1, sync=set('c'), numsync_confirmed=1, active=set('c'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('c')),
|
||||
])
|
||||
|
||||
def test_1233(self):
|
||||
"""Interrupted transition from 2 node cluster to 3 node fully sync cluster"""
|
||||
leader = 'a'
|
||||
|
||||
# Node c went away, transition back to 2 node cluster
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=1, active=set('b'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 1, set('b')),
|
||||
])
|
||||
|
||||
# Node c is available transition to larger quorum set, but not yet caught up.
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=1, active=set('bc'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 1, set('bc')),
|
||||
])
|
||||
|
||||
# Add in a new node at the same time, but node c didn't caught up yet
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=1, active=set('bcd'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 2, set('bcd')),
|
||||
('sync', leader, 2, set('bcd')),
|
||||
])
|
||||
# All sync nodes caught up, reduce quorum
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcd'),
|
||||
numsync=2, sync=set('bcd'), numsync_confirmed=3, active=set('bcd'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 1, set('bcd')),
|
||||
])
|
||||
|
||||
# Change replication factor at the same time
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('b'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=1, active=set('bc'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 1, set('bc')),
|
||||
('sync', leader, 1, set('bc')),
|
||||
])
|
||||
|
||||
def test_2322(self):
|
||||
"""Interrupted transition from 2 node cluster to 3 node cluster with replication factor 2"""
|
||||
leader = 'a'
|
||||
|
||||
# Node c went away, transition back to 2 node cluster
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('b'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('b')),
|
||||
])
|
||||
|
||||
# Node c is available transition to larger quorum set.
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('bc'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('sync', leader, 1, set('bc')),
|
||||
])
|
||||
|
||||
# Add in a new node at the same time
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('bcd'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('sync', leader, 1, set('bc')),
|
||||
('quorum', leader, 2, set('bcd')),
|
||||
('sync', leader, 1, set('bcd')),
|
||||
])
|
||||
|
||||
# Convert to a fully synced cluster
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=1, sync=set('b'), numsync_confirmed=1, active=set('bc'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bc')),
|
||||
])
|
||||
# Reduce quorum after all nodes caught up
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=2, active=set('bc'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('bc')),
|
||||
])
|
||||
|
||||
def test_3535(self):
|
||||
leader = 'a'
|
||||
|
||||
# remove nodes
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=2, active=set('bc'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bc')),
|
||||
('quorum', leader, 0, set('bc')),
|
||||
])
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=3, active=set('bcd'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bcd')),
|
||||
('quorum', leader, 1, set('bcd')),
|
||||
])
|
||||
|
||||
# remove nodes and decrease sync
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=2, active=set('bc'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bc')),
|
||||
('quorum', leader, 1, set('bc')),
|
||||
('sync', leader, 1, set('bc')),
|
||||
])
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bcde'),
|
||||
numsync=3, sync=set('bcde'), numsync_confirmed=2, active=set('bc'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('sync', leader, 3, set('bcd')),
|
||||
('quorum', leader, 1, set('bc')),
|
||||
('sync', leader, 1, set('bc')),
|
||||
])
|
||||
|
||||
# Increase replication factor and decrease quorum
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=2, active=set('bcde'),
|
||||
sync_wanted=3, leader_wanted=leader, expected=[
|
||||
('sync', leader, 3, set('bcde')),
|
||||
])
|
||||
# decrease quorum after more nodes caught up
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=3, sync=set('bcde'), numsync_confirmed=3, active=set('bcde'),
|
||||
sync_wanted=3, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 1, set('bcde')),
|
||||
])
|
||||
|
||||
# Add node with decreasing sync and increasing quorum
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=2, active=set('bcdef'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
# increase quorum by 2, 1 for added node and another for reduced sync
|
||||
('quorum', leader, 4, set('bcdef')),
|
||||
# now reduce replication factor to requested value
|
||||
('sync', leader, 1, set('bcdef')),
|
||||
])
|
||||
|
||||
# Remove node with increasing sync and decreasing quorum
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcde'),
|
||||
numsync=2, sync=set('bcde'), numsync_confirmed=2, active=set('bcd'),
|
||||
sync_wanted=3, leader_wanted=leader, expected=[
|
||||
# node e removed from sync wth replication factor increase
|
||||
('sync', leader, 3, set('bcd')),
|
||||
# node e removed from voters with quorum decrease
|
||||
('quorum', leader, 1, set('bcd')),
|
||||
])
|
||||
|
||||
def test_remove_nosync_node(self):
|
||||
leader = 'a'
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('bc'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=1, active=set('b'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('b')),
|
||||
('sync', leader, 1, set('b'))
|
||||
])
|
||||
|
||||
def test_swap_sync_node(self):
|
||||
leader = 'a'
|
||||
self.check_state_transitions(leader=leader, quorum=0, voters=set('bc'),
|
||||
numsync=2, sync=set('bc'), numsync_confirmed=1, active=set('bd'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set('b')),
|
||||
('sync', leader, 2, set('bd')),
|
||||
('quorum', leader, 1, set('bd'))
|
||||
])
|
||||
|
||||
def test_promotion(self):
|
||||
# Beginning stat: 'a' in the primary, 1 of bcd in sync
|
||||
# a fails, c gets quorum votes and promotes
|
||||
self.check_state_transitions(leader='a', quorum=2, voters=set('bcd'),
|
||||
numsync=0, sync=set(), numsync_confirmed=0, active=set(),
|
||||
sync_wanted=1, leader_wanted='c', expected=[
|
||||
('sync', 'a', 1, set('abd')), # set a and b to sync
|
||||
('quorum', 'c', 2, set('abd')), # set c as a leader and move a to voters
|
||||
# and stop because there are no active nodes
|
||||
])
|
||||
|
||||
# next loop, b managed to reconnect
|
||||
self.check_state_transitions(leader='c', quorum=2, voters=set('abd'),
|
||||
numsync=1, sync=set('abd'), numsync_confirmed=0, active=set('b'),
|
||||
sync_wanted=1, leader_wanted='c', expected=[
|
||||
('sync', 'c', 1, set('b')), # remove a from sync as inactive
|
||||
('quorum', 'c', 0, set('b')), # remove a from voters and reduce quorum
|
||||
])
|
||||
|
||||
# alternative reality: next loop, no one reconnected
|
||||
self.check_state_transitions(leader='c', quorum=2, voters=set('abd'),
|
||||
numsync=1, sync=set('abd'), numsync_confirmed=0, active=set(),
|
||||
sync_wanted=1, leader_wanted='c', expected=[
|
||||
('quorum', 'c', 0, set()),
|
||||
('sync', 'c', 0, set()),
|
||||
])
|
||||
|
||||
def test_nonsync_promotion(self):
|
||||
# Beginning state: 1 of bc in sync. e.g. (a primary, ssn = ANY 1 (b c))
|
||||
# a fails, d sees b and c, knows that it is in sync and decides to promote.
|
||||
# We include in sync state former primary increasing replication factor
|
||||
# and let situation resolve. Node d ssn=ANY 1 (b c)
|
||||
leader = 'd'
|
||||
self.check_state_transitions(leader='a', quorum=1, voters=set('bc'),
|
||||
numsync=0, sync=set(), numsync_confirmed=0, active=set(),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
# Set a, b, and c to sync and increase replication factor
|
||||
('sync', 'a', 2, set('abc')),
|
||||
# Set ourselves as the leader and move the old leader to voters
|
||||
('quorum', leader, 1, set('abc')),
|
||||
# and stop because there are no active nodes
|
||||
])
|
||||
# next loop, b and c managed to reconnect
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('abc'),
|
||||
numsync=2, sync=set('abc'), numsync_confirmed=0, active=set('bc'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('sync', leader, 2, set('bc')), # Remove a from being synced to.
|
||||
('quorum', leader, 1, set('bc')), # Remove a from quorum
|
||||
('sync', leader, 1, set('bc')), # Can now reduce replication factor back
|
||||
])
|
||||
# alternative reality: next loop, no one reconnected
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('abc'),
|
||||
numsync=2, sync=set('abc'), numsync_confirmed=0, active=set(),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 0, set()),
|
||||
('sync', leader, 0, set()),
|
||||
])
|
||||
|
||||
def test_invalid_states(self):
|
||||
leader = 'a'
|
||||
|
||||
# Main invariant is not satisfied, system is in an unsafe state
|
||||
resolver = QuorumStateResolver(leader=leader, quorum=0, voters=set('bc'),
|
||||
numsync=1, sync=set('bc'), numsync_confirmed=1,
|
||||
active=set('bc'), sync_wanted=1, leader_wanted=leader)
|
||||
self.assertRaises(QuorumError, resolver.check_invariants)
|
||||
self.assertEqual(list(resolver), [
|
||||
('quorum', leader, 1, set('bc'))
|
||||
])
|
||||
|
||||
# Quorum and sync states mismatched, somebody other than Patroni modified system state
|
||||
resolver = QuorumStateResolver(leader=leader, quorum=1, voters=set('bc'),
|
||||
numsync=2, sync=set('bd'), numsync_confirmed=1,
|
||||
active=set('bd'), sync_wanted=1, leader_wanted=leader)
|
||||
self.assertRaises(QuorumError, resolver.check_invariants)
|
||||
self.assertEqual(list(resolver), [
|
||||
('quorum', leader, 1, set('bd')),
|
||||
('sync', leader, 1, set('bd')),
|
||||
])
|
||||
self.assertTrue(repr(resolver.sync).startswith('<CaseInsensitiveSet'))
|
||||
|
||||
def test_sync_high_quorum_low_safety_margin_high(self):
|
||||
leader = 'a'
|
||||
|
||||
self.check_state_transitions(leader=leader, quorum=2, voters=set('bcdef'),
|
||||
numsync=4, sync=set('bcdef'), numsync_confirmed=3, active=set('bcdef'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
('quorum', leader, 3, set('bcdef')), # Adjust quorum requirements
|
||||
('sync', leader, 2, set('bcdef')), # Reduce synchronization
|
||||
])
|
||||
|
||||
def test_quorum_update(self):
|
||||
resolver = QuorumStateResolver(leader='a', quorum=1, voters=set('bc'), numsync=1, sync=set('bc'),
|
||||
numsync_confirmed=1, active=set('bc'), sync_wanted=1, leader_wanted='a')
|
||||
self.assertRaises(QuorumError, list, resolver.quorum_update(-1, set()))
|
||||
self.assertRaises(QuorumError, list, resolver.quorum_update(1, set()))
|
||||
|
||||
def test_sync_update(self):
|
||||
resolver = QuorumStateResolver(leader='a', quorum=1, voters=set('bc'), numsync=1, sync=set('bc'),
|
||||
numsync_confirmed=1, active=set('bc'), sync_wanted=1, leader_wanted='a')
|
||||
self.assertRaises(QuorumError, list, resolver.sync_update(-1, set()))
|
||||
self.assertRaises(QuorumError, list, resolver.sync_update(1, set()))
|
||||
|
||||
def test_remove_nodes_with_decreasing_sync(self):
|
||||
leader = 'a'
|
||||
|
||||
# Remove node with decreasing sync
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bcdef'),
|
||||
numsync=4, sync=set('bcdef'), numsync_confirmed=2, active=set('bcd'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
# node f removed from sync
|
||||
('sync', leader, 4, set('bcde')),
|
||||
# nodes e and f removed from voters with quorum decrease
|
||||
('quorum', leader, 1, set('bcd')),
|
||||
# node e removed from sync with replication factor decrease
|
||||
('sync', leader, 2, set('bcd')),
|
||||
])
|
||||
|
||||
# Interrupted state, and node g joined
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bcdef'),
|
||||
numsync=4, sync=set('bcde'), numsync_confirmed=2, active=set('bcdg'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
# remove nodes e and f from voters
|
||||
('quorum', leader, 1, set('bcd')),
|
||||
# remove node e from sync and reduce replication factor
|
||||
('sync', leader, 3, set('bcd')),
|
||||
# add node g to voters with quorum increase
|
||||
('quorum', leader, 2, set('bcdg')),
|
||||
# add node g to sync and reduce replication factor
|
||||
('sync', leader, 2, set('bcdg')),
|
||||
])
|
||||
|
||||
# node f returned
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bcdef'),
|
||||
numsync=4, sync=set('bcde'), numsync_confirmed=2, active=set('bcdf'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
# replace node e with f in sync
|
||||
('sync', leader, 4, set('bcdf')),
|
||||
# remove nodes e from voters with quorum decrease
|
||||
('quorum', leader, 2, set('bcdf')),
|
||||
# reduce replication factor as it was requested
|
||||
('sync', leader, 2, set('bcdf')),
|
||||
])
|
||||
|
||||
# node e returned
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bcdef'),
|
||||
numsync=4, sync=set('bcde'), numsync_confirmed=2, active=set('bcde'),
|
||||
sync_wanted=2, leader_wanted=leader, expected=[
|
||||
# remove nodes f from voters with quorum decrease
|
||||
('quorum', leader, 2, set('bcde')),
|
||||
# reduce replication factor as it was requested
|
||||
('sync', leader, 2, set('bcde')),
|
||||
])
|
||||
|
||||
# node b is also lost
|
||||
self.check_state_transitions(leader=leader, quorum=1, voters=set('bcdef'),
|
||||
numsync=4, sync=set('bcde'), numsync_confirmed=2, active=set('cd'),
|
||||
sync_wanted=1, leader_wanted=leader, expected=[
|
||||
# remove nodes b, e, and f from voters
|
||||
('quorum', leader, 1, set('cd')),
|
||||
# remove nodes b and e from sync with replication factor decrease
|
||||
('sync', leader, 1, set('cd')),
|
||||
])
|
||||
|
||||
def test_empty_ssn(self):
|
||||
# Beginning stat: 'a' in the primary, 1 of bc in sync
|
||||
# a fails, c gets quorum votes and promotes
|
||||
self.check_state_transitions(leader='a', quorum=1, voters=set('bc'),
|
||||
numsync=1, sync=set(), numsync_confirmed=0, active=set(),
|
||||
sync_wanted=1, leader_wanted='c', expected=[
|
||||
('sync', 'a', 1, set('ab')), # remove a from sync as inactive
|
||||
('quorum', 'c', 1, set('ab')), # set c as a leader and move a to voters
|
||||
# and stop because there are no active nodes
|
||||
])
|
||||
|
||||
# next loop, b managed to reconnect
|
||||
self.check_state_transitions(leader='c', quorum=1, voters=set('ab'),
|
||||
numsync=1, sync=set('ab'), numsync_confirmed=0, active=set('b'),
|
||||
sync_wanted=1, leader_wanted='c', expected=[
|
||||
('sync', 'c', 1, set('b')), # remove a from sync as inactive
|
||||
('quorum', 'c', 0, set('b')), # remove a from voters and reduce quorum
|
||||
])
|
||||
+2
-2
@@ -140,8 +140,8 @@ class TestRaft(unittest.TestCase):
|
||||
self.assertTrue(raft.initialize())
|
||||
self.assertTrue(raft.cancel_initialization())
|
||||
self.assertTrue(raft.set_config_value('{}'))
|
||||
self.assertTrue(raft.write_sync_state('foo', 'bar'))
|
||||
self.assertFalse(raft.write_sync_state('foo', 'bar', 1))
|
||||
self.assertTrue(raft.write_sync_state('foo', 'bar', 0))
|
||||
self.assertFalse(raft.write_sync_state('foo', 'bar', 0, 1))
|
||||
raft._mpp = get_mpp({'citus': {'group': 1, 'database': 'postgres'}})
|
||||
self.assertTrue(raft.manual_failover('foo', 'bar'))
|
||||
raft._mpp = get_mpp({'citus': {'group': 0, 'database': 'postgres'}})
|
||||
|
||||
+72
-15
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
from unittest.mock import Mock, patch, PropertyMock
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from patroni import global_config
|
||||
from patroni.collections import CaseInsensitiveSet
|
||||
from patroni.dcs import Cluster, SyncState
|
||||
from patroni.dcs import Cluster, ClusterConfig, Status, SyncState
|
||||
from patroni.postgresql import Postgresql
|
||||
|
||||
from . import BaseTestPostgresql, psycopg_connect, mock_available_gucs
|
||||
@@ -12,7 +13,6 @@ from . import BaseTestPostgresql, psycopg_connect, mock_available_gucs
|
||||
@patch('subprocess.call', Mock(return_value=0))
|
||||
@patch('patroni.psycopg.connect', psycopg_connect)
|
||||
@patch.object(Postgresql, 'available_gucs', mock_available_gucs)
|
||||
@patch.object(global_config.__class__, 'is_synchronous_mode', PropertyMock(return_value=True))
|
||||
class TestSync(BaseTestPostgresql):
|
||||
|
||||
@patch('subprocess.call', Mock(return_value=0))
|
||||
@@ -25,12 +25,13 @@ class TestSync(BaseTestPostgresql):
|
||||
super(TestSync, self).setUp()
|
||||
self.p.config.write_postgresql_conf()
|
||||
self.s = self.p.sync_handler
|
||||
config = ClusterConfig(1, {'synchronous_mode': True}, 1)
|
||||
self.cluster = Cluster(True, config, self.leader, Status.empty(), [self.me, self.other, self.leadermem],
|
||||
None, SyncState(0, self.me.name, self.leadermem.name, 0), None, None, None)
|
||||
global_config.update(self.cluster)
|
||||
|
||||
@patch.object(Postgresql, 'last_operation', Mock(return_value=1))
|
||||
def test_pick_sync_standby(self):
|
||||
cluster = Cluster(True, None, self.leader, 0, [self.me, self.other, self.leadermem], None,
|
||||
SyncState(0, self.me.name, self.leadermem.name), None, None, None)
|
||||
|
||||
pg_stat_replication = [
|
||||
{'pid': 100, 'application_name': self.leadermem.name, 'sync_state': 'sync', 'flush_lsn': 1},
|
||||
{'pid': 101, 'application_name': self.me.name, 'sync_state': 'async', 'flush_lsn': 2},
|
||||
@@ -39,34 +40,55 @@ class TestSync(BaseTestPostgresql):
|
||||
# sync node is a bit behind of async, but we prefer it anyway
|
||||
with patch.object(Postgresql, "_cluster_info_state_get", side_effect=[self.leadermem.name,
|
||||
'on', pg_stat_replication]):
|
||||
self.assertEqual(self.s.current_state(cluster), (CaseInsensitiveSet([self.leadermem.name]),
|
||||
CaseInsensitiveSet([self.leadermem.name])))
|
||||
self.assertEqual(self.s.current_state(self.cluster), ('priority', 1, 1,
|
||||
CaseInsensitiveSet([self.leadermem.name]),
|
||||
CaseInsensitiveSet([self.leadermem.name])))
|
||||
|
||||
# prefer node with sync_state='potential', even if it is slightly behind of async
|
||||
pg_stat_replication[0]['sync_state'] = 'potential'
|
||||
for r in pg_stat_replication:
|
||||
r['write_lsn'] = r.pop('flush_lsn')
|
||||
with patch.object(Postgresql, "_cluster_info_state_get", side_effect=['', 'remote_write', pg_stat_replication]):
|
||||
self.assertEqual(self.s.current_state(cluster), (CaseInsensitiveSet([self.leadermem.name]),
|
||||
CaseInsensitiveSet()))
|
||||
self.assertEqual(self.s.current_state(self.cluster), ('off', 0, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet([self.leadermem.name])))
|
||||
|
||||
# when there are no sync or potential candidates we pick async with the minimal replication lag
|
||||
for i, r in enumerate(pg_stat_replication):
|
||||
r.update(replay_lsn=3 - i, application_name=r['application_name'].upper())
|
||||
missing = pg_stat_replication.pop(0)
|
||||
with patch.object(Postgresql, "_cluster_info_state_get", side_effect=['', 'remote_apply', pg_stat_replication]):
|
||||
self.assertEqual(self.s.current_state(cluster), (CaseInsensitiveSet([self.me.name]), CaseInsensitiveSet()))
|
||||
self.assertEqual(self.s.current_state(self.cluster), ('off', 0, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet([self.me.name])))
|
||||
|
||||
# unknown sync node is ignored
|
||||
missing.update(application_name='missing', sync_state='sync')
|
||||
pg_stat_replication.insert(0, missing)
|
||||
with patch.object(Postgresql, "_cluster_info_state_get", side_effect=['', 'remote_apply', pg_stat_replication]):
|
||||
self.assertEqual(self.s.current_state(cluster), (CaseInsensitiveSet([self.me.name]), CaseInsensitiveSet()))
|
||||
self.assertEqual(self.s.current_state(self.cluster), ('off', 0, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet([self.me.name])))
|
||||
|
||||
# invalid synchronous_standby_names and empty pg_stat_replication
|
||||
with patch.object(Postgresql, "_cluster_info_state_get", side_effect=['a b', 'remote_apply', None]):
|
||||
self.p._major_version = 90400
|
||||
self.assertEqual(self.s.current_state(cluster), (CaseInsensitiveSet(), CaseInsensitiveSet()))
|
||||
self.assertEqual(self.s.current_state(self.cluster), ('off', 0, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet()))
|
||||
|
||||
@patch.object(Postgresql, 'last_operation', Mock(return_value=1))
|
||||
def test_current_state_quorum(self):
|
||||
self.cluster.config.data['synchronous_mode'] = 'quorum'
|
||||
global_config.update(self.cluster)
|
||||
|
||||
pg_stat_replication = [
|
||||
{'pid': 100, 'application_name': self.leadermem.name, 'sync_state': 'quorum', 'flush_lsn': 1},
|
||||
{'pid': 101, 'application_name': self.other.name, 'sync_state': 'quorum', 'flush_lsn': 2}]
|
||||
|
||||
# sync node is a bit behind of async, but we prefer it anyway
|
||||
with patch.object(Postgresql, "_cluster_info_state_get",
|
||||
side_effect=['ANY 1 ({0},"{1}")'.format(self.leadermem.name, self.other.name),
|
||||
'on', pg_stat_replication]):
|
||||
self.assertEqual(self.s.current_state(self.cluster),
|
||||
('quorum', 1, 2, CaseInsensitiveSet([self.other.name, self.leadermem.name]),
|
||||
CaseInsensitiveSet([self.leadermem.name, self.other.name])))
|
||||
|
||||
def test_set_sync_standby(self):
|
||||
def value_in_conf():
|
||||
@@ -85,6 +107,7 @@ class TestSync(BaseTestPostgresql):
|
||||
mock_reload.assert_not_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = 'n1'")
|
||||
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names(CaseInsensitiveSet(['n1', 'n2']))
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = '2 (n1,n2)'")
|
||||
@@ -99,11 +122,44 @@ class TestSync(BaseTestPostgresql):
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = '*'")
|
||||
|
||||
self.cluster.config.data['synchronous_mode'] = 'quorum'
|
||||
global_config.update(self.cluster)
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names([], 1)
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = 'ANY 1 (*)'")
|
||||
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names(['a', 'b'], 1)
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = 'ANY 1 (a,b)'")
|
||||
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names(['a', 'b'], 3)
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = 'ANY 3 (a,b)'")
|
||||
|
||||
self.p._major_version = 90601
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names([], 1)
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = '1 (*)'")
|
||||
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names(['a', 'b'], 1)
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = '1 (a,b)'")
|
||||
|
||||
mock_reload.reset_mock()
|
||||
self.s.set_synchronous_standby_names(['a', 'b'], 3)
|
||||
mock_reload.assert_called()
|
||||
self.assertEqual(value_in_conf(), "synchronous_standby_names = '3 (a,b)'")
|
||||
|
||||
@patch.object(Postgresql, 'last_operation', Mock(return_value=1))
|
||||
def test_do_not_prick_yourself(self):
|
||||
self.p.name = self.leadermem.name
|
||||
cluster = Cluster(True, None, self.leader, 0, [self.me, self.other, self.leadermem], None,
|
||||
SyncState(0, self.me.name, self.leadermem.name), None, None, None)
|
||||
SyncState(0, self.me.name, self.leadermem.name, 0), None, None, None)
|
||||
|
||||
pg_stat_replication = [
|
||||
{'pid': 100, 'application_name': self.leadermem.name, 'sync_state': 'sync', 'flush_lsn': 1},
|
||||
@@ -114,4 +170,5 @@ class TestSync(BaseTestPostgresql):
|
||||
# the pg_stat_replication. We need to check that primary is not selected as the synchronous node.
|
||||
with patch.object(Postgresql, "_cluster_info_state_get", side_effect=[self.leadermem.name,
|
||||
'on', pg_stat_replication]):
|
||||
self.assertEqual(self.s.current_state(cluster), (CaseInsensitiveSet([self.me.name]), CaseInsensitiveSet()))
|
||||
self.assertEqual(self.s.current_state(cluster), ('priority', 1, 0, CaseInsensitiveSet(),
|
||||
CaseInsensitiveSet([self.me.name])))
|
||||
|
||||
Reference in New Issue
Block a user