Skip to content

Commit

Permalink
Update static single node to be static primary & other changes
Browse files Browse the repository at this point in the history
This modifies the configuration pattern to use `static_primary=<node>`
instead of `static_single_node`. This allows for a more robust pattern
where even if replicas are added to the Patroni cluster, Patroni will
be able to protect itself from entering into unsafe states.
  • Loading branch information
thedodd committed Apr 22, 2022
1 parent 5713908 commit ffc5aaa
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 30 deletions.
22 changes: 11 additions & 11 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ jobs:

steps:
- uses: actions/checkout@v1
- name: Set up Python 2.7
uses: actions/setup-python@v2
with:
python-version: 2.7
if: matrix.os != 'windows'
- name: Install dependencies
run: python .github/workflows/install_deps.py
if: matrix.os != 'windows'
- name: Run tests and flake8
run: python .github/workflows/run_tests.py
if: matrix.os != 'windows'
# - name: Set up Python 2.7
# uses: actions/setup-python@v2
# with:
# python-version: 2.7
# if: matrix.os != 'windows'
# - name: Install dependencies
# run: python .github/workflows/install_deps.py
# if: matrix.os != 'windows'
# - name: Run tests and flake8
# run: python .github/workflows/run_tests.py
# if: matrix.os != 'windows'

- name: Set up Python 3.6
uses: actions/setup-python@v2
Expand Down
4 changes: 2 additions & 2 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Version 2.2.0

**New features**

- Added support for static_single_node configuration ``patronictl`` (Anthony Dodd)
- Added support for ``static_primary`` configuration (Anthony Dodd)

This can be configured using the ``static_single_node=True`` config value, which enables a few optimizations to ensure a single-node master does not demote unnecessarily.
This can be configured using the ``static_primary=<name>`` config value, which enables a few optimizations to ensure a single-node cluster does not demote its only node unnecessarily.

Version 2.1.3
-------------
Expand Down
10 changes: 7 additions & 3 deletions patroni/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class Config(object):
'check_timeline': False,
'master_start_timeout': 300,
'master_stop_timeout': 0,
'static_single_node': False,
'static_primary': None,
'synchronous_mode': False,
'synchronous_mode_strict': False,
'synchronous_node_count': 1,
Expand Down Expand Up @@ -235,7 +235,7 @@ def _safe_copy_dynamic_configuration(self, dynamic_configuration):
if name in self.__DEFAULT_CONFIG['standby_cluster']:
config['standby_cluster'][name] = deepcopy(value)
elif name in config: # only variables present in __DEFAULT_CONFIG allowed to be overridden from DCS
if name in ('synchronous_mode', 'synchronous_mode_strict'):
if name in ('synchronous_mode', 'synchronous_mode_strict', 'static_primary'):
config[name] = value
else:
config[name] = int(value)
Expand All @@ -248,7 +248,7 @@ def _build_environment_configuration():
def _popenv(name):
return os.environ.pop(PATRONI_ENV_PREFIX + name.upper(), None)

for param in ('name', 'namespace', 'scope', 'static_single_node'):
for param in ('name', 'namespace', 'scope', 'static_primary'):
value = _popenv(param)
if value:
ret[param] = value
Expand Down Expand Up @@ -429,6 +429,10 @@ def _build_effective_configuration(self, dynamic_configuration, local_configurat
if 'name' not in config and 'name' in pg_config:
config['name'] = pg_config['name']

# if 'static_primary' not in config and 'static_primary' in local_configuration
if 'static_primary' in local_configuration:
config['static_primary'] = local_configuration['static_primary']

updated_fields = (
'name',
'scope',
Expand Down
34 changes: 22 additions & 12 deletions patroni/ha.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,18 @@ def is_leader(self):
with self._is_leader_lock:
return self._is_leader > time.time()

def is_static_single_node(self):
if self.patroni.config is None:
def is_static_primary(self):
"""Check if this node is configured as the static primary of the cluster."""
static_primary = self.patroni.config.get('static_primary')
name = self.patroni.config.get('name')
if static_primary is None or name is None:
return False
# QUESTION to reviewers: should we check the last held DCS config as well just to ensure there is no conflict?
# Perhaps we should just force this value to only be considered as part of non-dynamic config, thoughts?
return self.patroni.config.get('static_single_node', False)
return static_primary == name

def is_static_primary_configured(self):
"""Check if the Patroni cluster has been configured with a static primary."""
static_primary = self.patroni.config.get('static_primary')
return static_primary is not None

def set_is_leader(self, value):
with self._is_leader_lock:
Expand Down Expand Up @@ -696,8 +702,8 @@ def _is_healthiest_node(self, members, check_replication_lag=True):
def is_failover_possible(self, members, check_synchronous=True, cluster_lsn=None):
ret = False
cluster_timeline = self.cluster.timeline
is_static_single_node = self.is_static_single_node()
members = [m for m in members if m.name != self.state_handler.name and not m.nofailover and m.api_url and not is_static_single_node]
is_static_primary = self.is_static_primary()
members = [m for m in members if m.name != self.state_handler.name and not m.nofailover and m.api_url and not is_static_primary]
if check_synchronous and self.is_synchronous_mode():
members = [m for m in members if self.cluster.sync.matches(m.name)]
if members:
Expand Down Expand Up @@ -998,7 +1004,7 @@ def process_unhealthy_cluster(self):
'promoted self to leader by acquiring session lock'
)
else:
if self.is_static_single_node():
if self.is_static_primary():
return 'no action as cluster is in static single node config mode'

return self.follow('demoted self after trying and failing to obtain lock',
Expand All @@ -1013,7 +1019,7 @@ def process_unhealthy_cluster(self):
if self.patroni.nofailover:
return self.follow('demoting self because I am not allowed to become master',
'following a different leader because I am not allowed to promote')
if self.is_static_single_node():
if self.is_static_primary():
return 'no action as cluster is in static single node config mode'
return self.follow('demoting self because i am not the healthiest node',
'following a different leader because i am not the healthiest node')
Expand Down Expand Up @@ -1055,8 +1061,8 @@ def process_healthy_cluster(self):
if self.state_handler.is_leader():
if self.is_paused():
return 'continue to run as master after failing to update leader lock in DCS'
if self.is_static_single_node():
return 'continue to run as master after failing to update leader lock in DCS due to static_single_node config'
if self.is_static_primary():
return 'continue to run as master after failing to update leader lock in DCS due to static_primary config'
self.demote('immediate-nolock')
return 'demoted self because failed to update leader lock in DCS'
else:
Expand Down Expand Up @@ -1360,6 +1366,10 @@ def _run_cycle(self):
self.state_handler.reset_cluster_info_state(None, self.patroni.nofailover)
raise

# If the cluster has been configured with a static primary, and we are not that primary, then do not proceed.
if self.is_static_primary_configured() and not self.is_static_primary():
return 'patroni cluster is configured with a static primary, and this node is not the primary, refusing to start'

if self.is_paused():
self.watchdog.disable()
self._was_paused = True
Expand Down Expand Up @@ -1501,7 +1511,7 @@ def _run_cycle(self):
except DCSError:
dcs_failed = True
logger.error('Error communicating with DCS')
if not self.is_paused() and self.state_handler.is_running() and self.state_handler.is_leader() and not self.is_static_single_node():
if not self.is_paused() and self.state_handler.is_running() and self.state_handler.is_leader() and not self.is_static_primary():
self.demote('offline')
return 'demoted self because DCS is not accessible and i was a leader'
return 'DCS is not accessible'
Expand Down
1 change: 1 addition & 0 deletions patroni/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def assert_(condition, message="Wrong value"):

schema = Schema({
"name": str,
Optional("static_primary"): str,
"scope": str,
"restapi": {
"listen": validate_host_port_listen,
Expand Down
3 changes: 1 addition & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_reload_local_configuration(self):
'PATRONI_LOGLEVEL': 'ERROR',
'PATRONI_LOG_LOGGERS': 'patroni.postmaster: WARNING, urllib3: DEBUG',
'PATRONI_LOG_FILE_NUM': '5',
'PATRONI_STATIC_SINGLE_NODE': 'True',
'PATRONI_STATIC_PRIMARY': 'postgres0',
'PATRONI_RESTAPI_USERNAME': 'username',
'PATRONI_RESTAPI_PASSWORD': 'password',
'PATRONI_RESTAPI_LISTEN': '0.0.0.0:8008',
Expand Down Expand Up @@ -74,7 +74,6 @@ def test_reload_local_configuration(self):
config.reload_local_configuration()
self.assertTrue(config.reload_local_configuration())
self.assertIsNone(config.reload_local_configuration())
self.assertTrue(config.static_single_node)

@patch('tempfile.mkstemp', Mock(return_value=[3000, 'blabla']))
@patch('os.path.exists', Mock(return_value=True))
Expand Down

0 comments on commit ffc5aaa

Please sign in to comment.