mirror of
https://github.com/optim-enterprises-bv/patroni.git
synced 2026-01-10 17:41:26 +00:00
1. when evaluating whether there are healthy nodes for a leader race before demoting we need to take into account quorum requirements. Without it the former leader may end up in recovery surrounded by asynchronous nodes.
2. QuorumStateResolver wasn't correctly handling the case when the replica node quickly joined and disconnected, what was resulting in the following errors:
```
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 427, in _generate_transitions
yield from self.__remove_gone_nodes()
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 327, in __remove_gone_nodes
yield from self.sync_update(numsync, sync)
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 227, in sync_update
raise QuorumError(f'Sync {numsync} > N of ({sync})')
patroni.quorum.QuorumError: Sync 2 > N of ({'postgresql2'})
2025-02-14 10:18:07,058 INFO: Unexpected exception raised, please report it as a BUG
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 246, in __iter__
transitions = list(self._generate_transitions())
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 423, in _generate_transitions
yield from self.__handle_non_steady_cases()
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 281, in __handle_non_steady_cases
yield from self.quorum_update(len(voters) - self.numsync, voters)
File "/home/akukushkin/git/patroni/patroni/quorum.py", line 184, in quorum_update
raise QuorumError(f'Quorum {quorum} < 0 of ({voters})')
patroni.quorum.QuorumError: Quorum -1 < 0 of ({'postgresql1'})
2025-02-18 15:50:48,243 INFO: Unexpected exception raised, please report it as a BUG
```
539 lines
27 KiB
Python
539 lines
27 KiB
Python
import itertools
|
|
import unittest
|
|
|
|
from typing import List, Set, Tuple
|
|
|
|
from patroni.quorum import QuorumError, QuorumStateResolver
|
|
|
|
|
|
class QuorumTest(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.nesting = 0
|
|
self.failures = []
|
|
|
|
def tearDown(self):
|
|
self.assertEqual(self.failures, [])
|
|
|
|
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:
|
|
self.nesting += 1
|
|
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
|
|
}
|
|
state = {k: v for k, v in kwargs.items()}
|
|
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(quorum=result[0][2], voters=result[0][3])
|
|
kwargs['expected'] = expected[1:]
|
|
self.check_state_transitions(**kwargs)
|
|
self.nesting -= 1
|
|
|
|
# Now we want to automatically check interruped states, emulating situation when new nodes joined and gone away.
|
|
# We are not going to compare exact state transitions, but rather check that they will not produce exceptions.
|
|
if self.nesting > 0 or not active > sync:
|
|
return
|
|
|
|
for e in expected:
|
|
if e[0] == 'restart' or e[1] != state['leader']:
|
|
return
|
|
if e[0] == 'sync':
|
|
state.update(numsync=e[2], sync=e[3])
|
|
else:
|
|
state.update(quorum=e[2], voters=e[3])
|
|
safety_margin = state['quorum'] + state['numsync'] - len(state['voters'] | state['sync'])
|
|
|
|
# we are only interested in non-steady cases, when quorum is higher than required by numsync
|
|
if safety_margin == 0:
|
|
return
|
|
|
|
# prepare initial state
|
|
state = {k: v for k, v in kwargs.items() if k != 'expected'}
|
|
|
|
def combinations(a):
|
|
for r in range(0, len(a) + 1):
|
|
for c in itertools.combinations(a, r):
|
|
yield set(c)
|
|
|
|
for e in expected:
|
|
if e[0] == 'sync':
|
|
state.update(numsync=e[2], sync=e[3])
|
|
else:
|
|
state.update(quorum=e[2], voters=e[3])
|
|
|
|
for a in combinations(sync):
|
|
# we will check cases with reverting back to active being subsets of sync nodes
|
|
state['active'] = a
|
|
for c in range(0, len(state['active']) + 1):
|
|
# in addition to that we want to consider cases with numsync_confirmed having different values
|
|
state['numsync_confirmed'] = c
|
|
try:
|
|
result = list(QuorumStateResolver(**state))
|
|
except Exception as e:
|
|
self.failures.append(e)
|
|
|
|
# besides, we want to make a difference between voters being empty and non-empty
|
|
if state['voters']:
|
|
voters = state['voters']
|
|
quorum = state['quorum']
|
|
state.update(voters=set(), quorum=0)
|
|
try:
|
|
result = list(QuorumStateResolver(**state))
|
|
except Exception as e:
|
|
self.failures.append(e)
|
|
state.update(voters=voters, quorum=quorum)
|
|
|
|
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 with 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
|
|
])
|