Convert roles to enums (#3303)

This commit is contained in:
Polina Bungina
2025-04-18 17:12:43 +02:00
committed by GitHub
parent 32934b205f
commit 6938c21ff7
22 changed files with 327 additions and 238 deletions
+3 -1
View File
@@ -212,13 +212,15 @@ class Patroni(AbstractPatroniDaemon, Tags):
the change and cache the new dynamic configuration values in ``patroni.dynamic.json`` file under Postgres data
directory.
"""
from patroni.postgresql.misc import PostgresqlRole
logger.info(self.ha.run_cycle())
if self.dcs.cluster and self.dcs.cluster.config and self.dcs.cluster.config.data \
and self.config.set_dynamic_configuration(self.dcs.cluster.config):
self.reload_config()
if self.postgresql.role != 'uninitialized':
if self.postgresql.role != PostgresqlRole.UNINITIALIZED:
self.config.save_cache()
self.schedule_next_run()
+21 -17
View File
@@ -30,7 +30,7 @@ from . import global_config, psycopg
from .__main__ import Patroni
from .dcs import Cluster
from .exceptions import PostgresConnectionException, PostgresException
from .postgresql.misc import postgres_version_to_int, PostgresqlState
from .postgresql.misc import postgres_version_to_int, PostgresqlRole, PostgresqlState
from .utils import cluster_as_json, deep_compare, enable_keepalive, parse_bool, \
parse_int, patch_config, Retry, RetryFailedError, split_host_port, tzutc, uri
@@ -324,17 +324,19 @@ class RestApiHandler(BaseHTTPRequestHandler):
is_lagging = leader_optime and leader_optime > replayed_location + max_replica_lag
replica_status_code = 200 if not patroni.noloadbalance and not is_lagging and \
response.get('role') == 'replica' and response.get('state') == PostgresqlState.RUNNING else 503
response.get('role') == PostgresqlRole.REPLICA and response.get('state') == PostgresqlState.RUNNING else 503
if not cluster and response.get('pause'):
leader_status_code = 200 if response.get('role') in ('primary', 'standby_leader') else 503
primary_status_code = 200 if response.get('role') == 'primary' else 503
standby_leader_status_code = 200 if response.get('role') == 'standby_leader' else 503
leader_status_code = 200 if response.get('role') in (PostgresqlRole.PRIMARY,
PostgresqlRole.STANDBY_LEADER) else 503
primary_status_code = 200 if response.get('role') == PostgresqlRole.PRIMARY else 503
standby_leader_status_code = 200 if response.get('role') == PostgresqlRole.STANDBY_LEADER else 503
elif patroni.ha.is_leader():
leader_status_code = 200
if config.is_standby_cluster:
primary_status_code = replica_status_code = 503
standby_leader_status_code = 200 if response.get('role') in ('replica', 'standby_leader') else 503
standby_leader_status_code =\
200 if response.get('role') in (PostgresqlRole.REPLICA, PostgresqlRole.STANDBY_LEADER) else 503
else:
primary_status_code = 200
standby_leader_status_code = 503
@@ -436,7 +438,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
"""
patroni: Patroni = self.server.patroni
is_primary = patroni.postgresql.role == 'primary' and patroni.postgresql.is_running()
is_primary = patroni.postgresql.role == PostgresqlRole.PRIMARY and patroni.postgresql.is_running()
# We can tolerate Patroni problems longer on the replica.
# On the primary the liveness probe most likely will start failing only after the leader key expired.
# It should not be a big problem because replicas will see that the primary is still alive via REST API call.
@@ -584,7 +586,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
metrics.append("# HELP patroni_primary Value is 1 if this node is the leader, 0 otherwise.")
metrics.append("# TYPE patroni_primary gauge")
metrics.append("patroni_primary{0} {1}".format(labels, int(postgres['role'] == 'primary')))
metrics.append("patroni_primary{0} {1}".format(labels, int(postgres['role'] == PostgresqlRole.PRIMARY)))
metrics.append("# HELP patroni_xlog_location Current location of the Postgres"
" transaction log, 0 if this node is not the leader.")
@@ -593,11 +595,12 @@ class RestApiHandler(BaseHTTPRequestHandler):
metrics.append("# HELP patroni_standby_leader Value is 1 if this node is the standby_leader, 0 otherwise.")
metrics.append("# TYPE patroni_standby_leader gauge")
metrics.append("patroni_standby_leader{0} {1}".format(labels, int(postgres['role'] == 'standby_leader')))
metrics.append("patroni_standby_leader{0} {1}".format(labels,
int(postgres['role'] == PostgresqlRole.STANDBY_LEADER)))
metrics.append("# HELP patroni_replica Value is 1 if this node is a replica, 0 otherwise.")
metrics.append("# TYPE patroni_replica gauge")
metrics.append("patroni_replica{0} {1}".format(labels, int(postgres['role'] == 'replica')))
metrics.append("patroni_replica{0} {1}".format(labels, int(postgres['role'] == PostgresqlRole.REPLICA)))
metrics.append("# HELP patroni_sync_standby Value is 1 if this node is a sync standby, 0 otherwise.")
metrics.append("# TYPE patroni_sync_standby gauge")
@@ -910,7 +913,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
status_code = _
break
elif k == 'role':
if request[k] not in ('primary', 'standby_leader', 'replica'):
if request[k] not in (PostgresqlRole.PRIMARY, PostgresqlRole.STANDBY_LEADER, PostgresqlRole.REPLICA):
status_code = 400
data = "PostgreSQL role should be either primary, standby_leader, or replica"
break
@@ -1266,16 +1269,17 @@ class RestApiHandler(BaseHTTPRequestHandler):
* ``state``: one of :class:`~patroni.postgresql.misc.PostgresqlState` or ``unknown``;
* ``postmaster_start_time``: ``pg_postmaster_start_time()``;
* ``role``: ``replica`` or ``primary`` based on ``pg_is_in_recovery()`` output;
* ``role``: :class:`~patroni.postgresql.misc.PostgresqlRole.REPLICA` or
:class:`~patroni.postgresql.misc.PostgresqlRole.PRIMARY` based on ``pg_is_in_recovery()`` output;
* ``server_version``: Postgres version without periods, e.g. ``150002`` for Postgres ``15.2``;
* ``latest_end_lsn``: latest_end_lsn value from ``pg_stat_get_wal_receiver()``, only on replica nodes;
* ``xlog``: dictionary. Its structure depends on ``role``:
* If ``primary``:
* If :class:`~patroni.postgresql.misc.PostgresqlRole.PRIMARY`:
* ``location``: ``pg_current_wal_flush_lsn()``
* If ``replica``:
* If :class:`~patroni.postgresql.misc.PostgresqlRole.REPLICA`:
* ``received_location``: ``pg_wal_lsn_diff(pg_last_wal_receive_lsn(), '0/0')``;
* ``replayed_location``: ``pg_wal_lsn_diff(pg_last_wal_replay_lsn(), '0/0)``;
@@ -1327,7 +1331,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
result = {
'state': postgresql.state,
'postmaster_start_time': row[0],
'role': 'replica' if row[1] == 0 else 'primary',
'role': PostgresqlRole.REPLICA if row[1] == 0 else PostgresqlRole.PRIMARY,
'server_version': postgresql.server_version,
'xlog': ({
'received_location': row[10] or row[4] or row[3],
@@ -1338,10 +1342,10 @@ class RestApiHandler(BaseHTTPRequestHandler):
})
}
if result['role'] == 'replica' and config.is_standby_cluster:
if result['role'] == PostgresqlRole.REPLICA and config.is_standby_cluster:
result['role'] = postgresql.role
if result['role'] == 'replica' and config.is_synchronous_mode\
if result['role'] == PostgresqlRole.REPLICA and config.is_synchronous_mode\
and cluster and cluster.sync.matches(postgresql.name):
result['quorum_standby' if global_config.is_quorum_commit_mode else 'sync_standby'] = True
+54 -35
View File
@@ -29,6 +29,7 @@ import time
from collections import defaultdict
from contextlib import contextmanager
from enum import Enum
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING, Union
from urllib.parse import urlparse
@@ -64,7 +65,7 @@ from . import global_config
from .config import Config
from .dcs import AbstractDCS, Cluster, get_dcs as _get_dcs, Member
from .exceptions import PatroniException
from .postgresql.misc import postgres_version_to_int, PostgresqlState
from .postgresql.misc import postgres_version_to_int, PostgresqlRole, PostgresqlState
from .postgresql.mpp import get_mpp
from .request import PatroniRequest
from .utils import cluster_as_json, patch_config, polling_loop
@@ -80,6 +81,20 @@ DCS_DEFAULTS: Dict[str, Dict[str, Any]] = {
'etcd3': {'port': 2379, 'template': "etcd3:\n host: '{host}:{port}'"}}
class CtlPostgresqlRole(str, Enum):
LEADER = 'leader'
PRIMARY = 'primary'
STANDBY_LEADER = 'standby-leader'
REPLICA = 'replica'
STANDBY = 'standby'
ANY = 'any'
def __repr__(self) -> str:
"""Get a string representation of a :class:`CtlPostgresqlRole` member."""
return self.value
class PatroniCtlException(click.ClickException):
"""Raised upon issues faced by ``patronictl`` utility."""
@@ -289,7 +304,7 @@ arg_cluster_name = click.argument('cluster_name', required=False,
option_default_citus_group = click.option('--group', required=False, type=int, help='Citus group',
default=lambda: _get_configuration().get('citus', {}).get('group'))
option_citus_group = click.option('--group', required=False, type=int, help='Citus group')
role_choice = click.Choice(['leader', 'primary', 'standby-leader', 'replica', 'standby', 'any'])
role_choice = click.Choice([role.value for role in CtlPostgresqlRole])
@click.group(cls=click.Group)
@@ -493,45 +508,42 @@ def watching(w: bool, watch: Optional[int], max_count: Optional[int] = None, cle
yield 0
def get_all_members(cluster: Cluster, group: Optional[int], role: str = 'leader') -> Iterator[Member]:
def get_all_members(cluster: Cluster, group: Optional[int],
role: CtlPostgresqlRole = CtlPostgresqlRole.LEADER) -> Iterator[Member]:
"""Get all cluster members that have the given *role*.
:param cluster: the Patroni cluster.
:param group: filter which Citus group we should get members from. If ``None`` get from all groups.
:param role: role to filter members. Can be one among:
* ``primary``: the primary PostgreSQL instance;
* ``replica`` or ``standby``: a standby PostgreSQL instance;
* ``leader``: the leader of a Patroni cluster. Can also be used to get the leader of a Patroni standby cluster;
* ``standby-leader``: the leader of a Patroni standby cluster;
* ``any``: matches any node independent of its role.
:param role: role to filter members. One of :class:`CtlPostgresqlRole` values.
:yields: members that have the given *role*.
"""
clusters = {0: cluster}
if is_citus_cluster() and group is None:
clusters.update(cluster.workers)
if role in ('leader', 'primary', 'standby-leader'):
if role in (CtlPostgresqlRole.LEADER, CtlPostgresqlRole.PRIMARY, CtlPostgresqlRole.STANDBY_LEADER):
# In the DCS the members' role can be one among: ``primary``, ``master``, ``replica`` or ``standby_leader``.
# ``primary`` and ``master`` are the same thing.
role = {'standby-leader': 'standby_leader'}.get(role, role)
for cluster in clusters.values():
if cluster.leader is not None and cluster.leader.name and\
(role == 'leader'
or cluster.leader.data.get('role') not in ('primary', 'master') and role == 'standby_leader'
or cluster.leader.data.get('role') != 'standby_leader' and role == 'primary'):
(role == CtlPostgresqlRole.LEADER
or cluster.leader.data.get('role') not in (PostgresqlRole.PRIMARY, PostgresqlRole.MASTER)
and role == CtlPostgresqlRole.STANDBY_LEADER
or cluster.leader.data.get('role') != PostgresqlRole.STANDBY_LEADER
and role == CtlPostgresqlRole.PRIMARY):
yield cluster.leader.member
return
for cluster in clusters.values():
leader_name = (cluster.leader.member.name if cluster.leader else None)
for m in cluster.members:
if role == 'any' or role in ('replica', 'standby') and m.name != leader_name:
if role == CtlPostgresqlRole.ANY or\
role in (CtlPostgresqlRole.REPLICA, CtlPostgresqlRole.STANDBY) and m.name != leader_name:
yield m
def get_any_member(cluster: Cluster, group: Optional[int],
role: Optional[str] = None, member: Optional[str] = None) -> Optional[Member]:
role: Optional[CtlPostgresqlRole] = None, member: Optional[str] = None) -> Optional[Member]:
"""Get the first found cluster member that has the given *role*.
:param cluster: the Patroni cluster.
@@ -547,9 +559,9 @@ def get_any_member(cluster: Cluster, group: Optional[int],
if member is not None:
if role is not None:
raise PatroniCtlException('--role and --member are mutually exclusive options')
role = 'any'
role = CtlPostgresqlRole.ANY
elif role is None:
role = 'leader'
role = CtlPostgresqlRole.LEADER
for m in get_all_members(cluster, group, role):
if member is None or m.name == member:
@@ -573,7 +585,8 @@ def get_all_members_leader_first(cluster: Cluster) -> Iterator[Member]:
def get_cursor(cluster: Cluster, group: Optional[int], connect_parameters: Dict[str, Any],
role: Optional[str] = None, member_name: Optional[str] = None) -> Union['cursor', 'Cursor[Any]', None]:
role: Optional[CtlPostgresqlRole] = None,
member_name: Optional[str] = None) -> Union['cursor', 'Cursor[Any]', None]:
"""Get a cursor object to execute queries against a member that has the given *role* or *member_name*.
.. note::
@@ -612,7 +625,7 @@ def get_cursor(cluster: Cluster, group: Optional[int], connect_parameters: Dict[
# If we want ``any`` node we are fine to return the cursor. ``None`` is similar to ``any`` at this point, as it's
# been dealt with through :func:`get_any_member`.
# If we want the Patroni leader node, :func:`get_any_member` already checks that for us
if role in (None, 'any', 'leader'):
if role in (None, CtlPostgresqlRole.ANY, CtlPostgresqlRole.LEADER):
return cursor
# If we want something other than ``any`` or ``leader``, then we do not rely only on the DCS information about
@@ -621,7 +634,9 @@ def get_cursor(cluster: Cluster, group: Optional[int], connect_parameters: Dict[
row = cursor.fetchone()
in_recovery = not row or row[0]
if in_recovery and role in ('replica', 'standby', 'standby-leader') or not in_recovery and role == 'primary':
if in_recovery and\
role in (CtlPostgresqlRole.REPLICA, CtlPostgresqlRole.STANDBY, CtlPostgresqlRole.STANDBY_LEADER) or\
not in_recovery and role == CtlPostgresqlRole.PRIMARY:
return cursor
conn.close()
@@ -629,7 +644,7 @@ def get_cursor(cluster: Cluster, group: Optional[int], connect_parameters: Dict[
return None
def get_members(cluster: Cluster, cluster_name: str, member_names: List[str], role: str,
def get_members(cluster: Cluster, cluster_name: str, member_names: List[str], role: CtlPostgresqlRole,
force: bool, action: str, ask_confirmation: bool = True, group: Optional[int] = None) -> List[Member]:
"""Get the list of members based on the given filters.
@@ -752,7 +767,7 @@ def confirm_members_action(members: List[Member], force: bool, action: str,
@click.option('--member', '-m', help='Generate a dsn for this member', type=str)
@arg_cluster_name
@option_citus_group
def dsn(cluster_name: str, group: Optional[int], role: Optional[str], member: Optional[str]) -> None:
def dsn(cluster_name: str, group: Optional[int], role: Optional[CtlPostgresqlRole], member: Optional[str]) -> None:
"""Process ``dsn`` command of ``patronictl`` utility.
Get DSN to connect to *member*.
@@ -798,7 +813,7 @@ def dsn(cluster_name: str, group: Optional[int], role: Optional[str], member: Op
def query(
cluster_name: str,
group: Optional[int],
role: Optional[str],
role: Optional[CtlPostgresqlRole],
member: Optional[str],
w: bool,
watch: Optional[int],
@@ -865,7 +880,7 @@ def query(
def query_member(cluster: Cluster, group: Optional[int], cursor: Union['cursor', 'Cursor[Any]', None],
member: Optional[str], role: Optional[str], command: str,
member: Optional[str], role: Optional[CtlPostgresqlRole], command: str,
connect_parameters: Dict[str, Any]) -> Tuple[List[List[Any]], Optional[List[Any]]]:
"""Execute SQL *command* against a member.
@@ -903,7 +918,7 @@ def query_member(cluster: Cluster, group: Optional[int], cursor: Union['cursor',
if member is not None:
message = f'No connection to member {member} is available'
elif role is not None:
message = f'No connection to role {role} is available'
message = f'No connection to role {role!r} is available'
else:
message = 'No connection is available'
logging.debug(message)
@@ -1030,9 +1045,11 @@ def parse_scheduled(scheduled: Optional[str]) -> Optional[datetime.datetime]:
@click.argument('cluster_name')
@click.argument('member_names', nargs=-1)
@option_citus_group
@click.option('--role', '-r', help='Reload only members with this role', type=role_choice, default='any')
@click.option('--role', '-r', help='Reload only members with this role',
type=role_choice, default=CtlPostgresqlRole.ANY)
@option_force
def reload(cluster_name: str, member_names: List[str], group: Optional[int], force: bool, role: str) -> None:
def reload(cluster_name: str, member_names: List[str], group: Optional[int],
force: bool, role: CtlPostgresqlRole) -> None:
"""Process ``reload`` command of ``patronictl`` utility.
Reload configuration of cluster members based on given filters.
@@ -1067,7 +1084,8 @@ def reload(cluster_name: str, member_names: List[str], group: Optional[int], for
@click.argument('cluster_name')
@click.argument('member_names', nargs=-1)
@option_citus_group
@click.option('--role', '-r', help='Restart only members with this role', type=role_choice, default='any')
@click.option('--role', '-r', help='Restart only members with this role', type=role_choice,
default=CtlPostgresqlRole.ANY)
@click.option('--any', 'p_any', help='Restart a single member only', is_flag=True)
@click.option('--scheduled', help='Timestamp of a scheduled restart in unambiguous format (e.g. ISO 8601)',
default=None)
@@ -1077,7 +1095,7 @@ def reload(cluster_name: str, member_names: List[str], group: Optional[int], for
@click.option('--timeout', help='Return error and fail over if necessary when restarting takes longer than this.')
@option_force
def restart(cluster_name: str, group: Optional[int], member_names: List[str],
force: bool, role: str, p_any: bool, scheduled: Optional[str], version: Optional[str],
force: bool, role: CtlPostgresqlRole, p_any: bool, scheduled: Optional[str], version: Optional[str],
pending: bool, timeout: Optional[str]) -> None:
"""Process ``restart`` command of ``patronictl`` utility.
@@ -1184,7 +1202,8 @@ def reinit(cluster_name: str, group: Optional[int], member_names: List[str], for
:param wait: wait for the operation to complete.
"""
cluster = get_dcs(cluster_name, group).get_cluster()
members = get_members(cluster, cluster_name, member_names, 'replica', force, 'reinitialize', group=group)
members = get_members(cluster, cluster_name, member_names, CtlPostgresqlRole.REPLICA,
force, 'reinitialize', group=group)
wait_on_members: List[Member] = []
for member in members:
@@ -1709,10 +1728,10 @@ def timestamp(precision: int = 6) -> str:
@option_citus_group
@click.argument('member_names', nargs=-1)
@click.argument('target', type=click.Choice(['restart', 'switchover']))
@click.option('--role', '-r', help='Flush only members with this role', type=role_choice, default='any')
@click.option('--role', '-r', help='Flush only members with this role', type=role_choice, default=CtlPostgresqlRole.ANY)
@option_force
def flush(cluster_name: str, group: Optional[int],
member_names: List[str], force: bool, role: str, target: str) -> None:
member_names: List[str], force: bool, role: CtlPostgresqlRole, target: str) -> None:
"""Process ``flush`` command of ``patronictl`` utility.
Discard scheduled restart or switchover events.
@@ -2193,7 +2212,7 @@ def version(cluster_name: str, group: Optional[int], member_names: List[str]) ->
click.echo("")
cluster = get_dcs(cluster_name, group).get_cluster()
for m in get_all_members(cluster, group, 'any'):
for m in get_all_members(cluster, group, CtlPostgresqlRole.ANY):
if m.api_url:
if not member_names or m.name in member_names:
try:
+37 -15
View File
@@ -25,6 +25,7 @@ from ..utils import deep_compare, parse_int, uri
if TYPE_CHECKING: # pragma: no cover
from ..config import Config
from ..postgresql import Postgresql
from ..postgresql.misc import PostgresqlRole
from ..postgresql.mpp import AbstractMPP
slot_name_re = re.compile('^[a-z0-9_]{1,63}$')
@@ -416,10 +417,13 @@ class Leader(NamedTuple):
>>> Leader(1, '', Member.from_node(1, '', '', '{"version":"z"}')).checkpoint_after_promote
"""
from ..postgresql.misc import PostgresqlRole
version = self.member.patroni_version
# 1.5.6 is the last version which doesn't expose checkpoint_after_promote: false
if version and version > (1, 5, 6):
return self.data.get('role') in ('master', 'primary') and 'checkpoint_after_promote' not in self.data
return self.data.get('role') in (PostgresqlRole.MASTER, PostgresqlRole.PRIMARY) \
and 'checkpoint_after_promote' not in self.data
return None
@@ -1010,7 +1014,8 @@ class Cluster(NamedTuple('Cluster',
return {name: value for name, value in self.__permanent_slots.items() if self.is_logical_slot(value)}
def get_replication_slots(self, postgresql: 'Postgresql', member: Tags, *,
role: Optional[str] = None, show_error: bool = False) -> Dict[str, Dict[str, Any]]:
role: Optional['PostgresqlRole'] = None,
show_error: bool = False) -> Dict[str, Dict[str, Any]]:
"""Lookup configured slot names in the DCS, report issues found and merge with permanent slots.
Will log an error if:
@@ -1019,7 +1024,8 @@ class Cluster(NamedTuple('Cluster',
:param postgresql: reference to :class:`Postgresql` object.
:param member: reference to an object implementing :class:`Tags` interface.
:param role: role of the node, if not set will be taken from *postgresql*.
:param role: role of the node, if not set will be taken from *postgresql*
One of :class:`~patroni.postgresql.misc.PostgresqlRole` values.
:param show_error: if ``True`` report error if any disabled logical slots or conflicting slot names are found.
:returns: final dictionary of slot names, after merging with permanent slots and performing sanity checks.
@@ -1041,7 +1047,7 @@ class Cluster(NamedTuple('Cluster',
return slots
def _merge_permanent_slots(self, slots: Dict[str, Dict[str, Any]], permanent_slots: Dict[str, Any],
name: str, role: str, can_advance_slots: bool) -> List[str]:
name: str, role: 'PostgresqlRole', can_advance_slots: bool) -> List[str]:
"""Merge replication *slots* for members with *permanent_slots*.
Perform validation of configured permanent slot name, skipping invalid names.
@@ -1051,13 +1057,15 @@ class Cluster(NamedTuple('Cluster',
:param slots: Slot names with existing attributes if known.
:param name: name of this node.
:param role: role of the node -- ``primary``, ``standby_leader`` or ``replica``.
:param role: role of the node. One of :class:`~patroni.postgresql.misc.PostgresqlRole` values.
:param permanent_slots: dictionary containing slot name key and slot information values.
:param can_advance_slots: ``True`` if ``pg_replication_slot_advance()`` function is available,
``False`` otherwise.
:returns: List of disabled permanent, logical slot names, if postgresql version < 11.
"""
from ..postgresql.misc import PostgresqlRole
name = slot_name_from_member_name(name)
topology = {slot_name_from_member_name(m.name): m.replicatefrom and slot_name_from_member_name(m.replicatefrom)
for m in self.members}
@@ -1084,7 +1092,8 @@ class Cluster(NamedTuple('Cluster',
# case we should have the following: A(B: active, C: inactive) <- B (C: active) <- C
# We don't consider the same situation on node B, because if node C doesn't exists, we will not
# be able to know its `replicatefrom` tag value.
expected_active = not topology.get(slot_name) and role in ('primary', 'standby_leader')
expected_active = not topology.get(slot_name) and role in (PostgresqlRole.PRIMARY,
PostgresqlRole.STANDBY_LEADER)
slots[slot_name] = {**value, 'expected_active': expected_active}
continue
@@ -1101,7 +1110,7 @@ class Cluster(NamedTuple('Cluster',
logger.error("Bad value for slot '%s' in permanent_slots: %s", slot_name, permanent_slots[slot_name])
return disabled_permanent_logical_slots
def _get_permanent_slots(self, postgresql: 'Postgresql', tags: Tags, role: str) -> Dict[str, Any]:
def _get_permanent_slots(self, postgresql: 'Postgresql', tags: Tags, role: 'PostgresqlRole') -> Dict[str, Any]:
"""Get configured permanent replication slots.
.. note::
@@ -1117,20 +1126,23 @@ class Cluster(NamedTuple('Cluster',
:param postgresql: reference to :class:`Postgresql` object.
:param tags: reference to an object implementing :class:`Tags` interface.
:param role: role of the node -- ``primary``, ``standby_leader`` or ``replica``.
:param role: role of the node. One of :class:`~patroni.postgresql.misc.PostgresqlRole` values.
:returns: dictionary of permanent slot names mapped to attributes.
"""
from ..postgresql.misc import PostgresqlRole
if not global_config.use_slots or tags.nofailover:
return {}
if global_config.is_standby_cluster or self.get_slot_name_on_primary(postgresql.name, tags) is None:
return self.permanent_physical_slots if postgresql.can_advance_slots or role == 'standby_leader' else {}
return self.permanent_physical_slots\
if postgresql.can_advance_slots or role == PostgresqlRole.STANDBY_LEADER else {}
return self.__permanent_slots if postgresql.can_advance_slots or role == 'primary' \
return self.__permanent_slots if postgresql.can_advance_slots or role == PostgresqlRole.PRIMARY \
else self.__permanent_logical_slots
def _get_members_slots(self, name: str, role: str, nofailover: bool,
def _get_members_slots(self, name: str, role: 'PostgresqlRole', nofailover: bool,
can_advance_slots: bool) -> Dict[str, Dict[str, Any]]:
"""Get physical replication slots configuration for a given member.
@@ -1162,6 +1174,8 @@ class Cluster(NamedTuple('Cluster',
:returns: dictionary of physical replication slots that should exist on a given node.
"""
from ..postgresql.misc import PostgresqlRole
if not global_config.use_slots:
return {}
@@ -1195,7 +1209,8 @@ class Cluster(NamedTuple('Cluster',
# In case when retention of replication slots is possible the `expected_active` function
# will be used to figure out whether the replication slot is expected to be active.
# Otherwise it will be used to find replication slots that should exist on a current node.
expected_active = leader_filter if role in ('primary', 'standby_leader') else replica_filter
expected_active = leader_filter if role in (PostgresqlRole.PRIMARY, PostgresqlRole.STANDBY_LEADER) \
else replica_filter
if can_advance_slots and global_config.member_slots_ttl > 0:
# if the node does only cascading and can't become the leader, we
@@ -1239,7 +1254,9 @@ class Cluster(NamedTuple('Cluster',
the node that we are checking permanent logical replication slots for.
:returns: ``True`` if there are permanent replication slots configured, otherwise ``False``.
"""
role = 'replica'
from ..postgresql.misc import PostgresqlRole
role = PostgresqlRole.REPLICA
members_slots: Dict[str, Dict[str, str]] = self._get_members_slots(postgresql.name, role,
member.nofailover,
postgresql.can_advance_slots)
@@ -1260,10 +1277,13 @@ class Cluster(NamedTuple('Cluster',
:param slots: slot names with LSN values.
:returns: a :class:`dict` object that contains only slots that are known to be permanent.
"""
from ..postgresql.misc import PostgresqlRole
if global_config.member_slots_ttl > 0:
return slots
permanent_slots: Dict[str, Any] = self._get_permanent_slots(postgresql, RemoteMember('', {}), 'replica')
permanent_slots: Dict[str, Any] = self._get_permanent_slots(postgresql, RemoteMember('', {}),
PostgresqlRole.REPLICA)
members_slots = {slot_name_from_member_name(m.name) for m in self.members}
return {name: value for name, value in slots.items() if name in permanent_slots
@@ -1279,7 +1299,9 @@ class Cluster(NamedTuple('Cluster',
:returns: ``True`` if any detected replications slots are ``logical``, otherwise ``False``.
"""
slots = self.get_replication_slots(postgresql, member, role='replica').values()
from ..postgresql.misc import PostgresqlRole
slots = self.get_replication_slots(postgresql, member, role=PostgresqlRole.REPLICA).values()
return any(v for v in slots if v.get("type") == "logical")
def should_enforce_hot_standby_feedback(self, postgresql: 'Postgresql', member: Tags) -> bool:
+4 -4
View File
@@ -19,7 +19,7 @@ from consul import base, Check, ConsulException, NotFound
from urllib3.exceptions import HTTPError
from ..exceptions import DCSError
from ..postgresql.misc import PostgresqlState
from ..postgresql.misc import PostgresqlRole, PostgresqlState
from ..postgresql.mpp import AbstractMPP
from ..utils import deep_compare, parse_bool, Retry, RetryFailedError, split_host_port, uri, USER_AGENT
from . import AbstractDCS, catch_return_false_exception, Cluster, ClusterConfig, \
@@ -533,8 +533,8 @@ class Consul(AbstractDCS):
check['TLSServerName'] = self._service_check_tls_server_name
tags = self._service_tags[:]
tags.append(role)
if role == 'primary':
tags.append('master')
if data['role'] == PostgresqlRole.PRIMARY:
tags.append(PostgresqlRole.MASTER)
self._previous_loop_service_tags = self._service_tags
self._previous_loop_token = self._client.token
@@ -552,7 +552,7 @@ class Consul(AbstractDCS):
return self.deregister_service(params['service_id'])
self._previous_loop_register_service = self._register_service
if role in ['primary', 'replica', 'standby-leader']:
if data['role'] in [PostgresqlRole.PRIMARY, PostgresqlRole.REPLICA, PostgresqlRole.STANDBY_LEADER]:
if state != PostgresqlState.RUNNING:
return
return self.register_service(service_name, **params)
+4 -3
View File
@@ -23,7 +23,7 @@ from urllib3.exceptions import HTTPError
from ..collections import EMPTY_DICT
from ..exceptions import DCSError
from ..postgresql.misc import PostgresqlState
from ..postgresql.misc import PostgresqlRole, PostgresqlState
from ..postgresql.mpp import AbstractMPP
from ..utils import deep_compare, iter_response_objects, \
keepalive_socket_options, Retry, RetryFailedError, tzutc, uri, USER_AGENT
@@ -1319,9 +1319,10 @@ class Kubernetes(AbstractDCS):
def touch_member(self, data: Dict[str, Any]) -> bool:
cluster = self.cluster
if cluster and cluster.leader and cluster.leader.name == self._name:
role = self._standby_leader_label_value if data['role'] == 'standby_leader' else self._leader_label_value
role = self._standby_leader_label_value \
if data['role'] == PostgresqlRole.STANDBY_LEADER else self._leader_label_value
tmp_role = 'primary'
elif data['state'] == PostgresqlState.RUNNING and data['role'] != 'primary':
elif data['state'] == PostgresqlState.RUNNING and data['role'] != PostgresqlRole.PRIMARY:
role = {'replica': self._follower_label_value}.get(data['role'], data['role'])
tmp_role = data['role']
else:
+29 -26
View File
@@ -17,7 +17,7 @@ from .collections import CaseInsensitiveSet
from .dcs import AbstractDCS, Cluster, Leader, Member, RemoteMember, Status, SyncState
from .exceptions import DCSError, PatroniFatalException, PostgresConnectionException
from .postgresql.callback_executor import CallbackAction
from .postgresql.misc import postgres_version_to_int, PostgresqlState
from .postgresql.misc import postgres_version_to_int, PostgresqlRole, PostgresqlState
from .postgresql.postmaster import PostmasterProcess
from .postgresql.rewind import Rewind
from .quorum import QuorumStateResolver
@@ -54,7 +54,8 @@ class _MemberStatus(Tags, NamedTuple('_MemberStatus',
# If one of those is not in a response we want to count the node as not healthy/reachable
wal: Dict[str, Any] = json.get('wal') or json['xlog']
# abuse difference in primary/replica response format
in_recovery = not (bool(wal.get('location')) or json.get('role') in ('master', 'primary'))
in_recovery = not (bool(wal.get('location'))
or json.get('role') in (PostgresqlRole.MASTER, PostgresqlRole.PRIMARY))
lsn = int(in_recovery and max(wal.get('received_location', 0), wal.get('replayed_location', 0)))
return cls(member, True, in_recovery, lsn, json)
@@ -474,7 +475,7 @@ class Ha(object):
# Unfortunately such optimization isn't possible on the standby_leader,
# therefore we will get the timeline from pg_control, either by calling
# pg_control_checkpoint() on 9.6+ or by parsing the output of pg_controldata.
if self.state_handler.role == 'standby_leader':
if self.state_handler.role == PostgresqlRole.STANDBY_LEADER:
timeline = pg_control_timeline or self.state_handler.pg_control_timeline()
else:
timeline = self.state_handler.replica_cached_timeline(self._leader_timeline) or 0
@@ -493,7 +494,7 @@ class Ha(object):
ret = self.dcs.touch_member(data)
if ret:
new_state = (data['state'], data['role'])
if self._last_state != new_state and new_state == (PostgresqlState.RUNNING, 'primary'):
if self._last_state != new_state and new_state == (PostgresqlState.RUNNING, PostgresqlRole.PRIMARY):
self.notify_mpp_coordinator('after_promote')
self._last_state = new_state
return ret
@@ -560,7 +561,7 @@ class Ha(object):
with self._async_response: # pretend that post_bootstrap was already executed
self._async_response.complete(result)
if result:
self.state_handler.set_role('standby_leader')
self.state_handler.set_role(PostgresqlRole.STANDBY_LEADER)
return result
@@ -635,7 +636,7 @@ class Ha(object):
and data.get('Database cluster state') in ('in production', 'in crash recovery',
'shutting down', 'shut down')\
and self.state_handler.state == PostgresqlState.CRASHED\
and self.state_handler.role == 'primary'\
and self.state_handler.role == PostgresqlRole.PRIMARY\
and not self.state_handler.config.recovery_conf_exists():
# We know 100% that we were running as a primary a few moments ago, therefore could just start postgres
msg = 'starting primary after failure'
@@ -654,7 +655,7 @@ class Ha(object):
self.load_cluster_from_dcs()
role = 'replica'
role = PostgresqlRole.REPLICA
if self.has_lock() and not self.is_standby_cluster():
self._rewind.reset_state() # we want to later trigger CHECKPOINT after promote
msg = "starting as readonly because i had the session lock"
@@ -668,7 +669,7 @@ class Ha(object):
if self.has_lock(): # in standby cluster
msg = "starting as a standby leader because i had the session lock"
role = 'standby_leader'
role = PostgresqlRole.STANDBY_LEADER
node_to_follow = self._get_node_to_follow(self.cluster)
elif self.is_standby_cluster() and self.cluster.is_unlocked():
msg = "trying to follow a remote member because standby cluster is unhealthy"
@@ -732,10 +733,10 @@ class Ha(object):
if not (self._rewind.is_needed and self._rewind.can_rewind_or_reinitialize_allowed)\
or self.cluster.is_unlocked():
if is_leader:
self.state_handler.set_role('primary')
self.state_handler.set_role(PostgresqlRole.PRIMARY)
return 'continue to run as primary without lock'
elif self.state_handler.role != 'standby_leader':
self.state_handler.set_role('replica')
elif self.state_handler.role != PostgresqlRole.STANDBY_LEADER:
self.state_handler.set_role(PostgresqlRole.REPLICA)
if not node_to_follow:
return 'no action. I am ({0})'.format(self.state_handler.name)
@@ -755,10 +756,12 @@ class Ha(object):
if not self.is_paused():
self.state_handler.handle_parameter_change()
role = 'standby_leader' if isinstance(node_to_follow, RemoteMember) and self.has_lock(False) else 'replica'
role = PostgresqlRole.STANDBY_LEADER \
if isinstance(node_to_follow, RemoteMember) and self.has_lock(False) else PostgresqlRole.REPLICA
# It might happen that leader key in the standby cluster references non-exiting member.
# In this case it is safe to continue running without changing recovery.conf
if self.is_standby_cluster() and role == 'replica' and not (node_to_follow and node_to_follow.conn_url):
if self.is_standby_cluster() and role == PostgresqlRole.REPLICA \
and not (node_to_follow and node_to_follow.conn_url):
return 'continue following the old known standby leader'
else:
change_required, restart_required = self.state_handler.config.check_recovery_conf(node_to_follow)
@@ -769,7 +772,7 @@ class Ha(object):
else:
self.state_handler.follow(node_to_follow, role, do_reload=True)
self._rewind.trigger_check_diverged_lsn()
elif role == 'standby_leader' and self.state_handler.role != role:
elif role == PostgresqlRole.STANDBY_LEADER and self.state_handler.role != role:
self.state_handler.set_role(role)
self.state_handler.call_nowait(CallbackAction.ON_ROLE_CHANGE)
@@ -1095,12 +1098,12 @@ class Ha(object):
if self.state_handler.is_primary():
# Inform the state handler about its primary role.
# It may be unaware of it if postgres is promoted manually.
self.state_handler.set_role('primary')
self.state_handler.set_role(PostgresqlRole.PRIMARY)
self.process_sync_replication()
self.update_cluster_history()
self.state_handler.mpp_handler.sync_meta_data(self.cluster)
return message
elif self.state_handler.role in ('primary', 'promoted'):
elif self.state_handler.role in (PostgresqlRole.PRIMARY, PostgresqlRole.PROMOTED):
self.process_sync_replication()
return message
else:
@@ -1108,7 +1111,7 @@ class Ha(object):
# Somebody else updated sync state, it may be due to us losing the lock. To be safe,
# postpone promotion until next cycle. TODO: trigger immediate retry of run_cycle.
return 'Postponing promotion because synchronous replication state was updated by somebody else'
if self.state_handler.role not in ('primary', 'promoted'):
if self.state_handler.role not in (PostgresqlRole.PRIMARY, PostgresqlRole.PROMOTED):
# reset failsafe state when promote
self._failsafe.set_is_active(0)
@@ -1156,7 +1159,7 @@ class Ha(object):
:returns: the reason why caller shouldn't continue as a primary or the current value of received/replayed LSN.
"""
if self.state_handler.state == PostgresqlState.RUNNING and self.state_handler.role == 'primary':
if self.state_handler.state == PostgresqlState.RUNNING and self.state_handler.role == PostgresqlRole.PRIMARY:
return 'Running as a leader'
self._failsafe.update(data)
return self._last_wal_lsn
@@ -1570,7 +1573,7 @@ class Ha(object):
# location, we can remove the leader key and allow them to start leader race.
time.sleep(1) # give replicas some more time to catch up
if self.is_failover_possible(cluster_lsn=checkpoint_location):
self.state_handler.set_role('demoted')
self.state_handler.set_role(PostgresqlRole.DEMOTED)
with self._async_executor:
self.release_leader_key_voluntarily(prev_location)
status['released'] = True
@@ -1586,7 +1589,7 @@ class Ha(object):
on_shutdown=on_shutdown if mode_control['release'] else None,
before_shutdown=before_shutdown if mode == 'graceful' else None,
stop_timeout=self.primary_stop_timeout())
self.state_handler.set_role('demoted')
self.state_handler.set_role(PostgresqlRole.DEMOTED)
self.set_is_leader(False)
if mode_control['release']:
@@ -1758,7 +1761,7 @@ class Ha(object):
# enforce anything, since the leader is not a primary
# So just remind the role.
msg = 'no action. I am ({0}), the standby leader with the lock'.format(self.state_handler.name) \
if self.state_handler.role == 'standby_leader' else \
if self.state_handler.role == PostgresqlRole.STANDBY_LEADER else \
'promoted self to a standby leader because i had the session lock'
return self.enforce_follow_remote_member(msg)
else:
@@ -1958,7 +1961,7 @@ class Ha(object):
self.state_handler.cancellable.cancel()
return 'lost leader before promote'
if self.state_handler.role == 'primary':
if self.state_handler.role == PostgresqlRole.PRIMARY:
logger.info('Demoting primary during %s', self._async_executor.scheduled_action)
if self._async_executor.scheduled_action in ('restart', 'starting primary after failure'):
# Restart needs a special interlocking cancel because postmaster may be just started in a
@@ -1984,8 +1987,8 @@ class Ha(object):
if not self.state_handler.is_running():
self.watchdog.disable()
if self.has_lock():
if self.state_handler.role in ('primary', 'standby_leader'):
self.state_handler.set_role('demoted')
if self.state_handler.role in (PostgresqlRole.PRIMARY, PostgresqlRole.STANDBY_LEADER):
self.state_handler.set_role(PostgresqlRole.DEMOTED)
self.state_handler.call_nowait(CallbackAction.ON_ROLE_CHANGE)
self._delete_leader()
return 'removed leader key after trying and failing to start postgres'
@@ -2010,7 +2013,7 @@ class Ha(object):
if not self.state_handler.is_primary():
return 'waiting for end of recovery after bootstrap'
self.state_handler.set_role('primary')
self.state_handler.set_role(PostgresqlRole.PRIMARY)
ret = self._async_executor.try_run_async('post_bootstrap', self.state_handler.bootstrap.post_bootstrap,
args=(self.patroni.config['bootstrap'], self._async_response))
return ret or 'running post_bootstrap'
@@ -2160,7 +2163,7 @@ class Ha(object):
data_directory_error = e
if not data_directory_is_accessible or data_directory_is_empty:
self.state_handler.set_role('uninitialized')
self.state_handler.set_role(PostgresqlRole.UNINITIALIZED)
self.state_handler.stop('immediate', stop_timeout=self.patroni.config['retry_timeout'])
# In case datadir went away while we were primary
self.watchdog.disable()
+28 -26
View File
@@ -28,7 +28,7 @@ from .callback_executor import CallbackAction, CallbackExecutor
from .cancellable import CancellableSubprocess
from .config import ConfigHandler, mtime
from .connection import ConnectionPool, get_connection_cursor
from .misc import parse_history, parse_lsn, postgres_major_version_to_int, PostgresqlState
from .misc import parse_history, parse_lsn, postgres_major_version_to_int, PostgresqlRole, PostgresqlState
from .mpp import AbstractMPP
from .postmaster import PostmasterProcess
from .slots import SlotsHandler
@@ -95,7 +95,7 @@ class Postgresql(object):
self.mpp_handler = mpp.get_handler_impl(self)
self._bin_dir = config.get('bin_dir') or ''
self._role_lock = Lock()
self.set_role('uninitialized')
self.set_role(PostgresqlRole.UNINITIALIZED)
self.config = ConfigHandler(self, config)
self.config.check_directories()
@@ -142,20 +142,20 @@ class Postgresql(object):
# we know that PostgreSQL is accepting connections and can read some GUC's from pg_settings
self.config.load_current_server_parameters()
self.set_role('primary' if self.is_primary() else 'replica')
self.set_role(PostgresqlRole.PRIMARY if self.is_primary() else PostgresqlRole.REPLICA)
hba_saved = self.config.replace_pg_hba()
ident_saved = self.config.replace_pg_ident()
if self.major_version < 120000 or self.role == 'primary':
if self.major_version < 120000 or self.role == PostgresqlRole.PRIMARY:
# If PostgreSQL is running as a primary or we run PostgreSQL that is older than 12 we can
# call reload_config() once again (the first call happened in the ConfigHandler constructor),
# so that it can figure out if config files should be updated and pg_ctl reload executed.
self.config.reload_config(config, sighup=bool(hba_saved or ident_saved))
elif hba_saved or ident_saved:
self.reload()
elif not self.is_running() and self.role == 'primary':
self.set_role('demoted')
elif not self.is_running() and self.role == PostgresqlRole.PRIMARY:
self.set_role(PostgresqlRole.DEMOTED)
@property
def create_replica_methods(self) -> List[str]:
@@ -237,7 +237,7 @@ class Postgresql(object):
" pg_catalog.pg_stat_get_activity(w.pid)"
" WHERE w.state = 'streaming') r)").format(self.wal_name, self.lsn_name)
if global_config.is_synchronous_mode
and self.role in ('primary', 'promoted') else "'on', '', NULL")
and self.role in (PostgresqlRole.PRIMARY, PostgresqlRole.PROMOTED) else "'on', '', NULL")
if self._major_version >= 90600:
filter_failover = ' WHERE NOT failover' if self._major_version >= 170000 else ''
@@ -252,7 +252,7 @@ class Postgresql(object):
if self._major_version >= 130000 else "NULL")
extra = (", CASE WHEN latest_end_lsn IS NULL THEN NULL ELSE received_tli END, {0}, slot_name, "
"conninfo, status, {1} FROM pg_catalog.pg_stat_get_wal_receiver()").format(written_lsn, extra)
if self.role == 'standby_leader':
if self.role == PostgresqlRole.STANDBY_LEADER:
extra = "timeline_id" + extra + ", pg_catalog.pg_control_checkpoint()"
else:
extra = "0" + extra
@@ -366,13 +366,13 @@ class Postgresql(object):
self._sysid = data.get('Database system identifier', '')
return self._sysid
def get_postgres_role_from_data_directory(self) -> str:
def get_postgres_role_from_data_directory(self) -> PostgresqlRole:
if self.data_directory_empty() or not self.controldata():
return 'uninitialized'
return PostgresqlRole.UNINITIALIZED
elif self.config.recovery_conf_exists():
return 'replica'
return PostgresqlRole.REPLICA
else:
return 'primary'
return PostgresqlRole.PRIMARY
@property
def server_version(self) -> int:
@@ -592,7 +592,7 @@ class Postgresql(object):
return bool(self._cluster_info_state_get('timeline'))
except PostgresConnectionException:
logger.warning('Failed to determine PostgreSQL state from the connection, falling back to cached role')
return bool(self.is_running() and self.role == 'primary')
return bool(self.is_running() and self.role == PostgresqlRole.PRIMARY)
def replay_paused(self) -> bool:
return self._cluster_info_state_get('replay_paused') or False
@@ -694,7 +694,7 @@ class Postgresql(object):
if self.callback and cb_type in self.callback:
cmd = self.callback[cb_type]
role = 'primary' if self.role == 'promoted' else self.role
role = PostgresqlRole.PRIMARY if self.role == PostgresqlRole.PROMOTED else self.role
try:
cmd = shlex.split(self.callback[cb_type]) + [cb_type, role, self.scope]
self._callback_executor.call(cmd)
@@ -702,11 +702,11 @@ class Postgresql(object):
logger.exception('callback %s %r %s %s failed', cmd, cb_type, role, self.scope)
@property
def role(self) -> str:
def role(self) -> PostgresqlRole:
with self._role_lock:
return self._role
def set_role(self, value: str) -> None:
def set_role(self, value: PostgresqlRole) -> None:
with self._role_lock:
self._role = value
@@ -747,7 +747,7 @@ class Postgresql(object):
return False
def start(self, timeout: Optional[float] = None, task: Optional[CriticalTask] = None,
block_callbacks: bool = False, role: Optional[str] = None,
block_callbacks: bool = False, role: Optional[PostgresqlRole] = None,
after_start: Optional[Callable[..., Any]] = None) -> Optional[bool]:
"""Start PostgreSQL
@@ -1032,7 +1032,7 @@ class Postgresql(object):
return self.state == PostgresqlState.RUNNING
def restart(self, timeout: Optional[float] = None, task: Optional[CriticalTask] = None,
block_callbacks: bool = False, role: Optional[str] = None,
block_callbacks: bool = False, role: Optional[PostgresqlRole] = None,
before_shutdown: Optional[Callable[..., Any]] = None,
after_start: Optional[Callable[..., Any]] = None) -> Optional[bool]:
"""Restarts PostgreSQL.
@@ -1140,14 +1140,15 @@ class Postgresql(object):
logger.exception('Failed to read and parse %s', (history_path,))
return history
def follow(self, member: Union[Leader, Member, None], role: str = 'replica',
def follow(self, member: Union[Leader, Member, None], role: PostgresqlRole = PostgresqlRole.REPLICA,
timeout: Optional[float] = None, do_reload: bool = False) -> Optional[bool]:
"""Reconfigure postgres to follow a new member or use different recovery parameters.
Method may call `on_role_change` callback if role is changing.
:param member: The member to follow
:param role: The desired role, normally 'replica', but could also be a 'standby_leader'
:param role: The desired role, one of :class:`~misc.PostgresqlRole` values, normally
:class:`~misc.PostgresqlRole.REPLICA`, but could also be a :class:`~misc.PostgresqlRole.STANDBY_LEADER`
:param timeout: start timeout, how long should the `start()` method wait for postgres accepting connections
:param do_reload: indicates that after updating postgresql.conf we just need to do a reload instead of restart
@@ -1165,8 +1166,9 @@ class Postgresql(object):
# and we know for sure that postgres was already running before, we will only execute on_role_change
# callback and prevent execution of on_restart/on_start callback.
# If the role remains the same (replica or standby_leader), we will execute on_start or on_restart
change_role = self.cb_called and (self.role in ('primary', 'demoted')
or not {'standby_leader', 'replica'} - {self.role, role})
change_role = self.cb_called and \
(self.role in (PostgresqlRole.PRIMARY, PostgresqlRole.DEMOTED)
or not {PostgresqlRole.STANDBY_LEADER, PostgresqlRole.REPLICA} - {self.role, role})
if change_role:
self.__cb_pending = CallbackAction.NOOP
@@ -1191,7 +1193,7 @@ class Postgresql(object):
for _ in polling_loop(wait_seconds):
data = self.controldata()
if data.get('Database cluster state') == 'in production':
self.set_role('primary')
self.set_role(PostgresqlRole.PRIMARY)
return True
def _pre_promote(self) -> bool:
@@ -1226,7 +1228,7 @@ class Postgresql(object):
def promote(self, wait_seconds: int, task: CriticalTask,
before_promote: Optional[Callable[..., Any]] = None) -> Optional[bool]:
if self.role in ('promoted', 'primary'):
if self.role in (PostgresqlRole.PROMOTED, PostgresqlRole.PRIMARY):
return True
ret = self._pre_promote()
@@ -1250,7 +1252,7 @@ class Postgresql(object):
ret = self.pg_ctl('promote', '-W')
if ret:
self.set_role('promoted')
self.set_role(PostgresqlRole.PROMOTED)
self.call_nowait(CallbackAction.ON_ROLE_CHANGE)
ret = self._wait_promote(wait_seconds)
return ret
@@ -1358,7 +1360,7 @@ class Postgresql(object):
logger.exception("Could not rename data directory %s", self._data_dir)
def remove_data_directory(self) -> None:
self.set_role('uninitialized')
self.set_role(PostgresqlRole.UNINITIALIZED)
logger.info('Removing data directory: %s', self._data_dir)
try:
if os.path.islink(self._data_dir):
+3 -3
View File
@@ -20,7 +20,7 @@ from ..psycopg import parse_conninfo
from ..utils import compare_values, get_postgres_version, is_subpath, \
maybe_convert_from_base_unit, parse_bool, parse_int, split_host_port, uri, validate_directory
from ..validator import EnumValidator, IntValidator
from .misc import get_major_from_minor_version, postgres_version_to_int, PostgresqlState
from .misc import get_major_from_minor_version, postgres_version_to_int, PostgresqlRole, PostgresqlState
from .validator import recovery_parameters, transform_postgresql_parameter_value, transform_recovery_parameter_value
if TYPE_CHECKING: # pragma: no cover
@@ -1062,7 +1062,7 @@ class ConfigHandler(object):
synchronous_standby_names = self._server_parameters.get('synchronous_standby_names')
if synchronous_standby_names is None:
if global_config.is_synchronous_mode_strict\
and self._postgresql.role in ('primary', 'promoted'):
and self._postgresql.role in (PostgresqlRole.PRIMARY, PostgresqlRole.PROMOTED):
parameters['synchronous_standby_names'] = '*'
else:
parameters.pop('synchronous_standby_names', None)
@@ -1330,7 +1330,7 @@ class ConfigHandler(object):
As a workaround we will start it with the values from controldata and set `pending_restart`
to true as an indicator that current values of parameters are not matching expectations."""
if self._postgresql.role == 'primary':
if self._postgresql.role == PostgresqlRole.PRIMARY:
return self._server_parameters
options_mapping = {
+16
View File
@@ -34,6 +34,22 @@ class PostgresqlState(str, Enum):
return self.value
class PostgresqlRole(str, Enum):
"""Possible values of :attr:`Postgresql.role`."""
PRIMARY = 'primary'
MASTER = 'master'
STANDBY_LEADER = 'standby_leader'
REPLICA = 'replica'
DEMOTED = 'demoted'
UNINITIALIZED = 'uninitialized'
PROMOTED = 'promoted'
def __repr__(self) -> str:
"""Get a string representation of a :class:`PostgresqlRole` member."""
return self.value
def postgres_version_to_int(pg_version: str) -> int:
"""Convert the server_version to integer
+3 -3
View File
@@ -9,7 +9,7 @@ from urllib.parse import urlparse
from ...dcs import Cluster
from ...psycopg import connect, ProgrammingError, quote_ident
from ...utils import parse_int
from ..misc import PostgresqlState
from ..misc import PostgresqlRole, PostgresqlState
from . import AbstractMPP, AbstractMPPHandler
if TYPE_CHECKING: # pragma: no cover
@@ -76,7 +76,7 @@ class PgDistNode:
:returns: ``True`` if this object represents the ``primary``.
"""
return self.role in ('primary', 'demoted')
return self.role in (PostgresqlRole.PRIMARY, PostgresqlRole.DEMOTED)
def as_tuple(self, include_nodeid: bool = False) -> Tuple[str, int, str, Optional[int]]:
"""Helper method to compare two :class:`PgDistGroup` objects.
@@ -478,7 +478,7 @@ class CitusHandler(Citus, AbstractMPPHandler, Thread):
for groupid, worker in cluster.workers.items():
leader = worker.leader
if leader and leader.conn_url\
and leader.data.get('role') in ('master', 'primary')\
and leader.data.get('role') in (PostgresqlRole.MASTER, PostgresqlRole.PRIMARY)\
and leader.data.get('state') == PostgresqlState.RUNNING:
self.add_task('after_promote', groupid, worker, leader.name, leader.conn_url)
+3 -2
View File
@@ -14,7 +14,7 @@ from ..collections import EMPTY_DICT
from ..dcs import Leader, RemoteMember
from . import Postgresql
from .connection import get_connection_cursor
from .misc import format_lsn, fsync_dir, parse_history, parse_lsn
from .misc import format_lsn, fsync_dir, parse_history, parse_lsn, PostgresqlRole
logger = logging.getLogger(__name__)
@@ -223,7 +223,8 @@ class Rewind(object):
if local_timeline is None or local_lsn is None:
return
if isinstance(leader, Leader) and leader.member.data.get('role') not in ('master', 'primary'):
if isinstance(leader, Leader) and leader.member.data.get('role') not in (PostgresqlRole.MASTER,
PostgresqlRole.PRIMARY):
return
# We want to use replication credentials when connecting to the "postgres" database in case if
+2 -2
View File
@@ -18,7 +18,7 @@ from ..file_perm import pg_perm
from ..psycopg import OperationalError
from ..tags import Tags
from .connection import get_connection_cursor
from .misc import format_lsn, fsync_dir
from .misc import format_lsn, fsync_dir, PostgresqlRole
if TYPE_CHECKING: # pragma: no cover
from psycopg import Cursor
@@ -693,7 +693,7 @@ class SlotsHandler:
leader = cluster.leader
if not leader:
return
slots = cluster.get_replication_slots(self._postgresql, tags, role='replica')
slots = cluster.get_replication_slots(self._postgresql, tags, role=PostgresqlRole.REPLICA)
copy_slots: Dict[str, Dict[str, Any]] = {}
with self._get_leader_connection_cursor(leader) as cur:
try:
+19 -15
View File
@@ -14,7 +14,7 @@ from patroni.dcs import ClusterConfig, Member
from patroni.exceptions import PostgresConnectionException
from patroni.ha import _MemberStatus
from patroni.postgresql.config import get_param_diff
from patroni.postgresql.misc import PostgresqlState
from patroni.postgresql.misc import PostgresqlRole, PostgresqlState
from patroni.psycopg import OperationalError
from patroni.utils import RetryFailedError, tzutc
@@ -51,7 +51,7 @@ class MockPostgresql:
connection_pool = MockConnectionPool()
name = 'test'
state = PostgresqlState.RUNNING
role = 'primary'
role = PostgresqlRole.PRIMARY
server_version = 90625
major_version = 90600
sysid = 'dummysysid'
@@ -214,21 +214,22 @@ class TestRestApiHandler(unittest.TestCase):
MockRestApiServer(RestApiHandler, 'GET /read-only')
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={})):
MockRestApiServer(RestApiHandler, 'GET /replica')
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'primary'})):
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': PostgresqlRole.PRIMARY})):
MockRestApiServer(RestApiHandler, 'GET /replica')
with patch.object(RestApiHandler, 'get_postgresql_status',
Mock(return_value={'state': PostgresqlState.RUNNING})):
MockRestApiServer(RestApiHandler, 'GET /health')
MockRestApiServer(RestApiHandler, 'GET /leader')
with patch.object(RestApiHandler, 'get_postgresql_status',
Mock(return_value={'role': 'replica', 'sync_standby': True})):
Mock(return_value={'role': PostgresqlRole.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})):
Mock(return_value={'role': PostgresqlRole.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'})):
with patch.object(RestApiHandler, 'get_postgresql_status',
Mock(return_value={'role': PostgresqlRole.REPLICA})):
MockRestApiServer(RestApiHandler, 'GET /asynchronous')
with patch.object(MockHa, 'is_leader', Mock(return_value=True)):
MockRestApiServer(RestApiHandler, 'GET /replica')
@@ -237,7 +238,7 @@ class TestRestApiHandler(unittest.TestCase):
with patch.object(global_config.__class__, 'is_standby_cluster', Mock(return_value=True)):
MockRestApiServer(RestApiHandler, 'GET /standby_leader')
MockPatroni.dcs.cluster = None
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'primary'})):
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': PostgresqlRole.PRIMARY})):
MockRestApiServer(RestApiHandler, 'GET /primary')
with patch.object(MockHa, 'restart_scheduled', Mock(return_value=True)):
MockRestApiServer(RestApiHandler, 'GET /primary')
@@ -264,7 +265,7 @@ class TestRestApiHandler(unittest.TestCase):
'tag_key1=true&tag_key2=false&'
'tag_key3=1&tag_key4=1.4&tag_key5=RandomTag&tag_key6=RandomTag2')
#
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'primary'})):
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': PostgresqlRole.PRIMARY})):
MockRestApiServer(RestApiHandler, 'GET /primary?lag=1M&'
'tag_key1=true&tag_key2=false&'
'tag_key3=1&tag_key4=1.4&tag_key5=RandomTag')
@@ -278,7 +279,8 @@ class TestRestApiHandler(unittest.TestCase):
'tag_key1=true&tag_key2=false&'
'tag_key3=1&tag_key4=1.4&tag_key5=RandomTag&tag_key6=RandomTag2')
#
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'standby_leader'})):
with patch.object(RestApiHandler, 'get_postgresql_status',
Mock(return_value={'role': PostgresqlRole.STANDBY_LEADER})):
MockRestApiServer(RestApiHandler, 'GET /standby_leader?lag=1M&'
'tag_key1=true&tag_key2=false&'
'tag_key3=1&tag_key4=1.4&tag_key5=RandomTag')
@@ -305,7 +307,7 @@ class TestRestApiHandler(unittest.TestCase):
'tag_key1=true&tag_key2=false&'
'tag_key3=1&tag_key4=1.4&tag_key5=RandomTag&tag_key6=RandomTag2')
#
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'primary'})):
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': PostgresqlRole.PRIMARY})):
MockRestApiServer(RestApiHandler, 'GET /replica?lag=1M&'
'tag_key1=true&tag_key2=false&'
'tag_key3=1&tag_key4=1.4&tag_key5=RandomTag')
@@ -461,27 +463,29 @@ class TestRestApiHandler(unittest.TestCase):
request = make_request(schedule=future_restart_time.isoformat(), role='unknown', postgres_version='9.5.3')
MockRestApiServer(RestApiHandler, request)
# wrong version
request = make_request(schedule=future_restart_time.isoformat(), role='primary', postgres_version='9.5.3.1')
request = make_request(schedule=future_restart_time.isoformat(),
role=PostgresqlRole.PRIMARY, postgres_version='9.5.3.1')
MockRestApiServer(RestApiHandler, request)
# unknown filter
request = make_request(schedule=future_restart_time.isoformat(), batman='lives')
MockRestApiServer(RestApiHandler, request)
# incorrect schedule
request = make_request(schedule='2016-08-42 12:45TZ+1', role='primary')
request = make_request(schedule='2016-08-42 12:45TZ+1', role=PostgresqlRole.PRIMARY)
MockRestApiServer(RestApiHandler, request)
# everything fine, but the schedule is missing
request = make_request(role='primary', postgres_version='9.5.2')
request = make_request(role=PostgresqlRole.PRIMARY, postgres_version='9.5.2')
MockRestApiServer(RestApiHandler, request)
for retval in (True, False):
with patch.object(MockHa, 'schedule_future_restart', Mock(return_value=retval)):
request = make_request(schedule=future_restart_time.isoformat())
MockRestApiServer(RestApiHandler, request)
with patch.object(MockHa, 'restart', Mock(return_value=(retval, "foo"))):
request = make_request(role='primary', postgres_version='9.5.2')
request = make_request(role=PostgresqlRole.PRIMARY, postgres_version='9.5.2')
MockRestApiServer(RestApiHandler, request)
with patch.object(global_config.__class__, 'is_paused', PropertyMock(return_value=True)):
MockRestApiServer(RestApiHandler, make_request(schedule='2016-08-42 12:45TZ+1', role='primary'))
MockRestApiServer(RestApiHandler,
make_request(schedule='2016-08-42 12:45TZ+1', role=PostgresqlRole.PRIMARY))
# Valid timeout
MockRestApiServer(RestApiHandler, make_request(timeout='60s'))
# Invalid timeout
+9 -3
View File
@@ -1,10 +1,12 @@
import unittest
from unittest import mock
from unittest.mock import Mock, patch
import psutil
from patroni.postgresql.callback_executor import CallbackExecutor
from patroni.postgresql.callback_executor import CallbackAction, CallbackExecutor
from patroni.postgresql.misc import PostgresqlRole
class TestCallbackExecutor(unittest.TestCase):
@@ -14,7 +16,7 @@ class TestCallbackExecutor(unittest.TestCase):
mock_popen.return_value.children.return_value = []
mock_popen.return_value.is_running.return_value = True
callback = ['test.sh', 'on_start', 'replica', 'foo']
callback = ['test.sh', CallbackAction.ON_START, PostgresqlRole.REPLICA, 'foo']
ce = CallbackExecutor()
ce._kill_children = Mock(side_effect=Exception)
ce._invoke_excepthook = Mock()
@@ -22,6 +24,8 @@ class TestCallbackExecutor(unittest.TestCase):
ce.join()
self.assertIsNone(ce.call(callback))
mock_popen.assert_called_with(['test.sh', 'on_start', 'replica', 'foo'], close_fds=True)
mock_popen.reset_mock()
mock_popen.return_value.kill.side_effect = psutil.AccessDenied()
self.assertIsNone(ce.call(callback))
@@ -38,5 +42,7 @@ class TestCallbackExecutor(unittest.TestCase):
self.assertIsNone(ce.call(callback))
mock_popen.side_effect = [Mock()]
self.assertIsNone(ce.call(['test.sh', 'on_reload', 'replica', 'foo']))
self.assertIsNone(ce.call(['test.sh', CallbackAction.ON_RELOAD, PostgresqlRole.REPLICA, 'foo']))
self.assertEqual(mock_popen.call_args_list[-2],
mock.call(['test.sh', 'on_reload', 'replica', 'foo'], close_fds=True))
ce.join()
+5 -4
View File
@@ -9,7 +9,7 @@ from consul import ConsulException, NotFound
from patroni.dcs import get_dcs
from patroni.dcs.consul import AbstractDCS, Cluster, Consul, ConsulClient, ConsulError, \
ConsulInternalError, HTTPClient, InvalidSession, InvalidSessionTTL, RetryFailedError
from patroni.postgresql.misc import PostgresqlState
from patroni.postgresql.misc import PostgresqlRole, PostgresqlState
from patroni.postgresql.mpp import get_mpp
from . import SleepException
@@ -257,7 +257,8 @@ class TestConsul(unittest.TestCase):
@patch.object(Agent.Service, 'register', Mock(side_effect=(False, True, True, True)))
@patch.object(Agent.Service, 'deregister', Mock(return_value=True))
def test_update_service(self):
d = {'role': 'replica', 'api_url': 'http://a/t', 'conn_url': 'pg://c:1', 'state': PostgresqlState.RUNNING}
d = {'role': PostgresqlRole.REPLICA, 'api_url': 'http://a/t', 'conn_url': 'pg://c:1',
'state': PostgresqlState.RUNNING}
self.assertIsNone(self.c.update_service({}, {}))
self.assertFalse(self.c.update_service({}, d))
self.assertTrue(self.c.update_service(d, d))
@@ -269,7 +270,7 @@ class TestConsul(unittest.TestCase):
d['state'] = PostgresqlState.RUNNING
d['role'] = 'bla'
self.assertIsNone(self.c.update_service({}, d))
d['role'] = 'primary'
d['role'] = PostgresqlRole.PRIMARY
self.assertTrue(self.c.update_service({}, d))
@patch.object(KV, 'put', Mock(side_effect=ConsulException))
@@ -281,7 +282,7 @@ class TestConsul(unittest.TestCase):
self.c.refresh_session = Mock(return_value=False)
d = {'role': 'replica', 'api_url': 'http://a/t',
d = {'role': PostgresqlRole.REPLICA, 'api_url': 'http://a/t',
'conn_url': 'pg://c:1', 'state': PostgresqlState.RUNNING}
# Changing register_service from True to False calls deregister()
+15 -9
View File
@@ -11,6 +11,7 @@ import etcd
from click.testing import CliRunner
from prettytable import PrettyTable
from patroni.ctl import CtlPostgresqlRole
from patroni.postgresql.misc import PostgresqlState
try:
@@ -52,7 +53,7 @@ def get_default_config(*args):
@patch('patroni.ctl.load_config', get_default_config)
@patch('patroni.dcs.AbstractDCS.get_cluster', Mock(return_value=get_cluster_initialized_with_leader()))
class TestCtl(unittest.TestCase):
TEST_ROLES = ('primary', 'leader')
TEST_ROLES = (CtlPostgresqlRole.PRIMARY, CtlPostgresqlRole.LEADER)
@patch('socket.getaddrinfo', socket_getaddrinfo)
def setUp(self):
@@ -89,14 +90,16 @@ class TestCtl(unittest.TestCase):
self.assertIsNotNone(get_cursor(get_cluster_initialized_with_leader(), None, {}, role=role))
# MockCursor returns pg_is_in_recovery as false
self.assertIsNone(get_cursor(get_cluster_initialized_with_leader(), None, {}, role='replica'))
self.assertIsNone(
get_cursor(get_cluster_initialized_with_leader(), None, {}, role=CtlPostgresqlRole.REPLICA))
self.assertIsNotNone(get_cursor(get_cluster_initialized_with_leader(), None, {'dbname': 'foo'}, role='any'))
self.assertIsNotNone(
get_cursor(get_cluster_initialized_with_leader(), None, {'dbname': 'foo'}, role=CtlPostgresqlRole.ANY))
# Mutually exclusive options
with self.assertRaises(PatroniCtlException) as e:
get_cursor(get_cluster_initialized_with_leader(), None, {'dbname': 'foo'}, member_name='other',
role='replica')
role=CtlPostgresqlRole.REPLICA)
self.assertEqual(str(e.exception), '--role and --member are mutually exclusive options')
@@ -301,7 +304,8 @@ class TestCtl(unittest.TestCase):
self.assertTrue('False' in str(rows))
with patch.object(MockCursor, 'execute', Mock(side_effect=OperationalError('bla'))):
rows = query_member(None, None, None, None, 'replica', 'SELECT pg_catalog.pg_is_in_recovery()', {})
rows = query_member(None, None, None, None,
CtlPostgresqlRole.REPLICA, 'SELECT pg_catalog.pg_is_in_recovery()', {})
with patch('patroni.ctl.get_cursor', Mock(return_value=None)):
# No role nor member given -- generic message
@@ -313,11 +317,13 @@ class TestCtl(unittest.TestCase):
self.assertTrue('No connection to member foo' in str(rows))
# Role given -- message pointing to role
rows = query_member(None, None, None, None, 'replica', 'SELECT pg_catalog.pg_is_in_recovery()', {})
rows = query_member(None, None, None, None,
CtlPostgresqlRole.REPLICA, 'SELECT pg_catalog.pg_is_in_recovery()', {})
self.assertTrue('No connection to role replica' in str(rows))
with patch('patroni.ctl.get_cursor', Mock(side_effect=OperationalError('bla'))):
rows = query_member(None, None, None, None, 'replica', 'SELECT pg_catalog.pg_is_in_recovery()', {})
rows = query_member(None, None, None, None,
CtlPostgresqlRole.REPLICA, 'SELECT pg_catalog.pg_is_in_recovery()', {})
def test_dsn(self):
result = self.runner.invoke(ctl, ['dsn', 'alpha'])
@@ -471,12 +477,12 @@ class TestCtl(unittest.TestCase):
self.assertEqual(len(r), 1)
self.assertEqual(r[0].name, 'leader')
r = list(get_all_members(get_cluster_initialized_with_leader(), None, role='replica'))
r = list(get_all_members(get_cluster_initialized_with_leader(), None, role=CtlPostgresqlRole.REPLICA))
self.assertEqual(len(r), 1)
self.assertEqual(r[0].name, 'other')
self.assertEqual(len(list(get_all_members(get_cluster_initialized_without_leader(),
None, role='replica'))), 2)
None, role=CtlPostgresqlRole.REPLICA))), 2)
def test_members(self):
result = self.runner.invoke(ctl, ['list'])
+32 -32
View File
@@ -18,7 +18,7 @@ from patroni.postgresql.bootstrap import Bootstrap
from patroni.postgresql.callback_executor import CallbackAction
from patroni.postgresql.cancellable import CancellableSubprocess
from patroni.postgresql.config import ConfigHandler
from patroni.postgresql.misc import PostgresqlState
from patroni.postgresql.misc import PostgresqlRole, PostgresqlState
from patroni.postgresql.postmaster import PostmasterProcess
from patroni.postgresql.rewind import Rewind
from patroni.postgresql.slots import SlotsHandler
@@ -59,7 +59,7 @@ def get_cluster_bootstrapping_without_leader(cluster_config=None):
def get_cluster_initialized_without_leader(leader=False, failover=None, sync=None, cluster_config=None, failsafe=False):
m1 = Member(0, 'leader', 28, {'conn_url': 'postgres://replicator:[email protected]:5435/postgres',
'api_url': 'http://127.0.0.1:8008/patroni', 'xlog_location': 4,
'role': 'primary', 'state': 'running'})
'role': PostgresqlRole.PRIMARY, 'state': 'running'})
leader = Leader(0, 0, m1 if leader else Member(0, '', 28, {}))
m2 = Member(0, 'other', 28, {'conn_url': 'postgres://replicator:[email protected]:5436/postgres',
'api_url': 'http://127.0.0.1:8011/patroni',
@@ -214,7 +214,7 @@ class TestHa(PostgresInit):
def setUp(self):
super(TestHa, self).setUp()
self.p.set_state(PostgresqlState.RUNNING)
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.p.postmaster_start_time = MagicMock(return_value=str(postmaster_start_time))
self.p.can_create_replica_without_replication_connection = MagicMock(return_value=False)
self.e = get_dcs({'etcd': {'ttl': 30, 'host': 'ok:2379', 'scope': 'test',
@@ -242,9 +242,9 @@ class TestHa(PostgresInit):
with patch.object(Postgresql, '_cluster_info_state_get', Mock(return_value='streaming')):
self.ha.touch_member()
self.p.timeline_wal_position = Mock(return_value=(0, 1, 1))
self.p.set_role('standby_leader')
self.p.set_role(PostgresqlRole.STANDBY_LEADER)
self.ha.touch_member()
self.p.set_role('primary')
self.p.set_role(PostgresqlRole.PRIMARY)
self.ha.dcs.touch_member = true
self.ha.touch_member()
@@ -299,7 +299,7 @@ class TestHa(PostgresInit):
self.p.follow = false
self.p.is_running = false
self.p.name = 'leader'
self.p.set_role('demoted')
self.p.set_role(PostgresqlRole.DEMOTED)
self.p.controldata = lambda: {'Database cluster state': 'shut down', 'Database system identifier': SYSID}
self.ha.cluster = get_cluster_initialized_with_leader()
self.assertEqual(self.ha.run_cycle(), 'starting as readonly because i had the session lock')
@@ -308,7 +308,7 @@ class TestHa(PostgresInit):
self.p.start = false
self.p.is_running = false
self.p.name = 'leader'
self.p.set_role('primary')
self.p.set_role(PostgresqlRole.PRIMARY)
self.p.controldata = lambda: {'Database cluster state': 'in production', 'Database system identifier': SYSID}
self.ha.cluster = get_cluster_initialized_with_leader()
self.assertEqual(self.ha.run_cycle(), 'starting primary after failure')
@@ -346,7 +346,7 @@ class TestHa(PostgresInit):
def test_recover_with_rewind(self):
self.p.is_running = false
self.ha.cluster = get_cluster_initialized_with_leader()
self.ha.cluster.leader.member.data.update(version='2.0.2', role='primary')
self.ha.cluster.leader.member.data.update(version='2.0.2', role=PostgresqlRole.PRIMARY)
self.ha._rewind.pg_rewind = true
self.ha._rewind.check_leader_is_not_in_recovery = true
with patch.object(Rewind, 'rewind_or_reinitialize_needed_and_possible', Mock(return_value=True)):
@@ -414,7 +414,7 @@ class TestHa(PostgresInit):
def test_long_promote(self):
self.ha.has_lock = true
self.p.is_primary = false
self.p.set_role('primary')
self.p.set_role(PostgresqlRole.PRIMARY)
self.assertEqual(self.ha.run_cycle(), 'no action. I am (postgresql0), the leader with the lock')
def test_demote_after_failing_to_obtain_lock(self):
@@ -579,7 +579,7 @@ class TestHa(PostgresInit):
def test_update_failsafe(self):
self.assertRaises(Exception, self.ha.update_failsafe, {})
self.p.set_role('primary')
self.p.set_role(PostgresqlRole.PRIMARY)
self.assertEqual(self.ha.update_failsafe({}), 'Running as a leader')
def test_call_failsafe_member(self):
@@ -715,7 +715,7 @@ class TestHa(PostgresInit):
self.assertEqual(self.ha.run_cycle(), 'updated leader lock during restart')
self.ha.update_lock = false
self.p.set_role('primary')
self.p.set_role(PostgresqlRole.PRIMARY)
with patch('patroni.async_executor.CriticalTask.cancel', Mock(return_value=False)), \
patch('patroni.async_executor.CriticalTask.result',
PropertyMock(return_value=PostmasterProcess(os.getpid())), create=True), \
@@ -907,7 +907,7 @@ class TestHa(PostgresInit):
def test_manual_failover_process_no_leader(self):
self.p.is_primary = false
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
# failover to another member, fetch_node_status for candidate fails
with patch('patroni.ha.logger.warning') as mock_warning:
@@ -917,7 +917,7 @@ class TestHa(PostgresInit):
('%s: member %s is %s', 'manual failover', 'leader', 'not reachable'))
# failover to another member, candidate is accessible, in_recovery
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.ha.fetch_node_status = get_node_status()
self.assertEqual(self.ha.run_cycle(), 'following a different leader because i am not the healthiest node')
@@ -928,7 +928,7 @@ class TestHa(PostgresInit):
self.assertEqual(self.ha.run_cycle(), 'promoted self to leader by acquiring session lock')
# failover to me but I am set to nofailover. In no case I should be elected as a leader
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.ha.cluster = get_cluster_initialized_without_leader(failover=Failover(0, '', 'postgresql0', None))
self.ha.patroni.nofailover = True
self.assertEqual(self.ha.run_cycle(), 'following a different leader because I am not allowed to promote')
@@ -952,7 +952,7 @@ class TestHa(PostgresInit):
def test_manual_switchover_process_no_leader(self):
self.p.is_primary = false
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
# I was the leader, other members are healthy
self.ha.fetch_node_status = get_node_status()
@@ -989,7 +989,7 @@ class TestHa(PostgresInit):
# manual failover to our node (postgresql0),
# which name is not in sync nodes list (some sync nodes are available)
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.ha.cluster = get_cluster_initialized_without_leader(failover=Failover(0, '', 'postgresql0', None),
sync=('leader1', 'other'))
self.p.sync_handler.current_state = Mock(return_value=(CaseInsensitiveSet(['leader1']),
@@ -1045,7 +1045,7 @@ class TestHa(PostgresInit):
# switchover to me, I am not leader
self.p.is_primary = false
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.ha.cluster = get_cluster_initialized_without_leader(failover=Failover(0, 'leader', self.p.name, None))
self.assertEqual(self.ha.run_cycle(), 'PAUSE: promoted self to leader by acquiring session lock')
@@ -1117,9 +1117,9 @@ class TestHa(PostgresInit):
def test_post_recover(self, mock_call_nowait):
self.p.is_running = false
self.ha.has_lock = true
self.p.set_role('primary')
self.p.set_role(PostgresqlRole.PRIMARY)
self.assertEqual(self.ha.post_recover(), 'removed leader key after trying and failing to start postgres')
self.assertEqual(self.p.role, 'demoted')
self.assertEqual(self.p.role, PostgresqlRole.DEMOTED)
mock_call_nowait.assert_called_once_with(CallbackAction.ON_ROLE_CHANGE)
self.ha.has_lock = false
self.assertEqual(self.ha.post_recover(), 'failed to start postgres')
@@ -1167,7 +1167,7 @@ class TestHa(PostgresInit):
self.assertEqual(self.ha.run_cycle(), "restart scheduled")
def test_restart_matches(self):
self.p._role = 'replica'
self.p._role = PostgresqlRole.REPLICA
self.p._connection.server_version = 90500
self.p._pending_restart = True
self.assertFalse(self.ha.restart_matches("primary", "9.5.0", True))
@@ -1195,7 +1195,7 @@ class TestHa(PostgresInit):
self.ha._leader_timeline = 1
self.assertEqual(self.ha.run_cycle(), 'promoted self to a standby leader because i had the session lock')
self.assertEqual(self.ha.run_cycle(), 'no action. I am (leader), the standby leader with the lock')
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.p.config.check_recovery_conf = Mock(return_value=(True, False))
self.assertEqual(self.ha.run_cycle(), 'promoted self to a standby leader because i had the session lock')
@@ -1221,7 +1221,7 @@ class TestHa(PostgresInit):
@patch.object(Rewind, 'can_rewind', PropertyMock(return_value=True))
def test_process_unhealthy_standby_cluster_as_cascade_replica(self):
self.p.is_primary = false
self.p.name = 'replica'
self.p.name = PostgresqlRole.REPLICA
self.ha.cluster = get_standby_cluster_initialized_with_only_leader()
self.assertTrue(self.ha.run_cycle().startswith('running pg_rewind from remote_member:'))
@@ -1458,7 +1458,7 @@ class TestHa(PostgresInit):
mock_set_sync = self.p.sync_handler.set_synchronous_standby_names = Mock()
self.p.is_primary = false
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.ha.has_lock = true
mock_write_sync = self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
self.p.name = 'leader'
@@ -1472,7 +1472,7 @@ class TestHa(PostgresInit):
mock_set_sync.reset_mock()
# When we just became primary nobody is sync
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
mock_write_sync.return_value = False
self.assertTrue(self.ha.enforce_primary_role('msg', 'promote msg') != 'promote msg')
mock_set_sync.assert_not_called()
@@ -1481,7 +1481,7 @@ class TestHa(PostgresInit):
self.ha.is_synchronous_mode = true
self.p.is_primary = false
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.p.name = 'other'
self.ha.cluster = get_cluster_initialized_without_leader(sync=('leader', 'other2'))
mock_write_sync = self.ha.dcs.write_sync_state = Mock(return_value=SyncState.empty())
@@ -1512,7 +1512,7 @@ class TestHa(PostgresInit):
self.p.name = 'other'
self.p.is_primary = false
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
mock_restart = self.p.restart = Mock(return_value=True)
self.ha.cluster = get_cluster_initialized_with_leader(sync=('leader', 'other'))
self.ha.touch_member = Mock(return_value=True)
@@ -1627,7 +1627,7 @@ class TestHa(PostgresInit):
self.p.data_directory_empty = Mock(side_effect=OSError(5, "Input/output error: '{}'".format(self.p.data_dir)))
self.assertEqual(self.ha.run_cycle(),
'released leader key voluntarily as data dir not accessible and currently leader')
self.assertEqual(self.p.role, 'uninitialized')
self.assertEqual(self.p.role, PostgresqlRole.UNINITIALIZED)
# as has_lock is mocked out, we need to fake the leader key release
self.ha.has_lock = false
@@ -1651,7 +1651,7 @@ class TestHa(PostgresInit):
self.p.is_primary = false
self.ha.run_cycle()
exit_mock.assert_called_once_with(1)
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.ha.dcs.initialize = Mock()
with patch.object(Postgresql, 'cb_called', PropertyMock(return_value=True)):
self.assertEqual(self.ha.run_cycle(), 'promoted self to leader by acquiring session lock')
@@ -1687,7 +1687,7 @@ class TestHa(PostgresInit):
self.ha.is_paused = true
self.p.data_directory_empty = true
self.assertEqual(self.ha.run_cycle(), 'PAUSE: running with empty data directory')
self.assertEqual(self.p.role, 'uninitialized')
self.assertEqual(self.p.role, PostgresqlRole.UNINITIALIZED)
@patch('patroni.ha.Ha.sysid_valid', MagicMock(return_value=True))
def test_sysid_no_match_in_pause(self):
@@ -1742,7 +1742,7 @@ class TestHa(PostgresInit):
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')
self.p.set_role(PostgresqlRole.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(),
@@ -1764,7 +1764,7 @@ class TestHa(PostgresInit):
self.p._major_version = 90600
mock_set_sync.reset_mock()
mock_write_sync.reset_mock()
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.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)
@@ -1773,7 +1773,7 @@ class TestHa(PostgresInit):
self.p._major_version = 150000
mock_set_sync.reset_mock()
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.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
+14 -14
View File
@@ -15,7 +15,7 @@ from patroni.dcs import get_dcs
from patroni.dcs.kubernetes import Cluster, k8s_client, k8s_config, K8sConfig, K8sConnectionFailed, \
K8sException, K8sObject, Kubernetes, KubernetesError, KubernetesRetriableException, Retry, \
RetryFailedError, SERVICE_HOST_ENV_NAME, SERVICE_PORT_ENV_NAME
from patroni.postgresql.misc import PostgresqlState
from patroni.postgresql.misc import PostgresqlRole, PostgresqlState
from patroni.postgresql.mpp import get_mpp
from . import MockResponse, SleepException
@@ -322,19 +322,19 @@ class TestKubernetesConfigMaps(BaseTestKubernetes):
mock_patch_namespaced_pod.return_value.metadata.resource_version = '10'
self.k._name = 'p-1'
self.k.touch_member({'role': 'replica', 'state': PostgresqlState.INITDB})
self.k.touch_member({'role': PostgresqlRole.REPLICA, 'state': PostgresqlState.INITDB})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], 'bar')
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': 'replica'})
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': PostgresqlRole.REPLICA})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], None)
self.k.touch_member({'role': 'replica', 'state': PostgresqlState.CUSTOM_BOOTSTRAP})
self.k.touch_member({'role': PostgresqlRole.REPLICA, 'state': PostgresqlState.CUSTOM_BOOTSTRAP})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], 'bar')
self.k.touch_member({'role': 'replica', 'state': PostgresqlState.BOOTSTRAP_STARTING})
self.k.touch_member({'role': PostgresqlRole.REPLICA, 'state': PostgresqlState.BOOTSTRAP_STARTING})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], 'bar')
self.k.touch_member({'state': PostgresqlState.STOPPED, 'role': 'primary'})
self.k.touch_member({'state': PostgresqlState.STOPPED, 'role': PostgresqlRole.PRIMARY})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], None)
self.k._role_label = 'isMaster'
@@ -343,26 +343,26 @@ class TestKubernetesConfigMaps(BaseTestKubernetes):
self.k._standby_leader_label_value = 'false'
self.k._tmp_role_label = 'tmp_role'
self.k.touch_member({'state': PostgresqlState.CREATING_REPLICA, 'role': 'replica'})
self.k.touch_member({'state': PostgresqlState.CREATING_REPLICA, 'role': PostgresqlRole.REPLICA})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], 'bar')
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': 'replica'})
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': PostgresqlRole.REPLICA})
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['foo'], None)
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['isMaster'], 'false')
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['tmp_role'], 'replica')
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['tmp_role'], PostgresqlRole.REPLICA)
mock_patch_namespaced_pod.rest_mock()
self.k._name = 'p-0'
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': 'standby_leader'})
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': PostgresqlRole.STANDBY_LEADER})
mock_patch_namespaced_pod.assert_called()
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['isMaster'], 'false')
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['tmp_role'], 'primary')
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['tmp_role'], PostgresqlRole.PRIMARY)
mock_patch_namespaced_pod.rest_mock()
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': 'primary'})
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': PostgresqlRole.PRIMARY})
mock_patch_namespaced_pod.assert_called()
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['isMaster'], 'true')
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['tmp_role'], 'primary')
self.assertEqual(mock_patch_namespaced_pod.call_args[0][2].metadata.labels['tmp_role'], PostgresqlRole.PRIMARY)
def test_initialize(self):
self.k.initialize()
@@ -495,7 +495,7 @@ class TestKubernetesEndpoints(BaseTestKubernetes):
self.assertEqual(('create_config_service failed',), mock_logger_exception.call_args[0])
mock_logger_exception.reset_mock()
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': 'replica'})
self.k.touch_member({'state': PostgresqlState.RUNNING, 'role': PostgresqlRole.REPLICA})
mock_logger_exception.assert_called_once()
self.assertEqual(('create_config_service failed',), mock_logger_exception.call_args[0])
+2 -2
View File
@@ -20,7 +20,7 @@ from patroni.dcs.etcd import AbstractEtcdClientWithFailover
from patroni.exceptions import DCSError
from patroni.postgresql import Postgresql
from patroni.postgresql.config import ConfigHandler
from patroni.postgresql.misc import PostgresqlState
from patroni.postgresql.misc import PostgresqlRole, PostgresqlState
from . import psycopg_connect, SleepException
from .test_etcd import etcd_read, etcd_write
@@ -163,7 +163,7 @@ class TestPatroni(unittest.TestCase):
@patch.object(Postgresql, 'state', PropertyMock(return_value=PostgresqlState.RUNNING))
@patch.object(Postgresql, 'data_directory_empty', Mock(return_value=False))
def test_run(self):
self.p.postgresql.set_role('replica')
self.p.postgresql.set_role(PostgresqlRole.REPLICA)
self.p.sighup_handler()
self.p.ha.dcs.watch = Mock(side_effect=SleepException)
self.p.api.start = Mock()
+7 -6
View File
@@ -22,7 +22,7 @@ from patroni.postgresql import PgIsReadyStatus, Postgresql
from patroni.postgresql.bootstrap import Bootstrap
from patroni.postgresql.callback_executor import CallbackAction
from patroni.postgresql.config import _false_validator, get_param_diff
from patroni.postgresql.misc import PostgresqlState
from patroni.postgresql.misc import PostgresqlRole, PostgresqlState
from patroni.postgresql.postmaster import PostmasterProcess
from patroni.postgresql.validator import _get_postgres_guc_validators, _load_postgres_gucs_validators, \
_read_postgres_gucs_validators_file, Bool, Enum, EnumBool, Integer, InvalidGucValidatorsFile, \
@@ -446,7 +446,7 @@ class TestPostgresql(BaseTestPostgresql):
task = CriticalTask()
self.assertTrue(self.p.promote(0, task))
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.p.config._config['pre_promote'] = 'test'
with patch('patroni.postgresql.cancellable.CancellableSubprocess.is_cancelled', PropertyMock(return_value=1)):
self.assertFalse(self.p.promote(0, task))
@@ -480,7 +480,7 @@ class TestPostgresql(BaseTestPostgresql):
@patch('shlex.split', Mock(side_effect=OSError))
def test_call_nowait(self):
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.assertIsNone(self.p.call_nowait(CallbackAction.ON_START))
self.p.bootstrapping = True
self.assertIsNone(self.p.call_nowait(CallbackAction.ON_START))
@@ -508,7 +508,7 @@ class TestPostgresql(BaseTestPostgresql):
@patch('os.path.exists', Mock(return_value=True))
@patch.object(Postgresql, 'controldata', Mock())
def test_get_postgres_role_from_data_directory(self):
self.assertEqual(self.p.get_postgres_role_from_data_directory(), 'replica')
self.assertEqual(self.p.get_postgres_role_from_data_directory(), PostgresqlRole.REPLICA)
@patch('os.remove', Mock())
@patch('shutil.rmtree', Mock())
@@ -852,7 +852,8 @@ class TestPostgresql(BaseTestPostgresql):
def test_get_primary_timeline(self):
self.assertEqual(self.p.get_primary_timeline(), 1)
@patch.object(Postgresql, 'get_postgres_role_from_data_directory', Mock(return_value='replica'))
@patch.object(Postgresql, 'get_postgres_role_from_data_directory',
Mock(return_value=PostgresqlRole.REPLICA))
@patch.object(Postgresql, 'is_running', Mock(return_value=False))
@patch.object(Bootstrap, 'running_custom_bootstrap', PropertyMock(return_value=True))
@patch('patroni.postgresql.config.logger')
@@ -893,7 +894,7 @@ class TestPostgresql(BaseTestPostgresql):
@patch.object(Postgresql, '_query', Mock(side_effect=RetryFailedError('')))
def test_received_timeline(self):
self.p.set_role('standby_leader')
self.p.set_role(PostgresqlRole.STANDBY_LEADER)
self.p.reset_cluster_info_state(None)
self.assertRaises(PostgresConnectionException, self.p.received_timeline)
+17 -16
View File
@@ -8,7 +8,7 @@ from unittest.mock import Mock, patch, PropertyMock
from patroni import global_config, psycopg
from patroni.dcs import Cluster, ClusterConfig, Member, Status, SyncState
from patroni.postgresql import Postgresql
from patroni.postgresql.misc import fsync_dir, PostgresqlState
from patroni.postgresql.misc import fsync_dir, PostgresqlRole, PostgresqlState
from patroni.postgresql.slots import SlotsAdvanceThread, SlotsHandler
from patroni.tags import Tags
@@ -52,21 +52,22 @@ class TestSlotsHandler(BaseTestPostgresql):
global_config.update(cluster)
with mock.patch('patroni.postgresql.Postgresql._query', Mock(side_effect=psycopg.OperationalError)):
self.s.sync_replication_slots(cluster, self.tags)
self.p.set_role('standby_leader')
self.p.set_role(PostgresqlRole.STANDBY_LEADER)
with patch.object(SlotsHandler, 'drop_replication_slot', Mock(return_value=(True, False))), \
patch.object(global_config.__class__, 'is_standby_cluster', PropertyMock(return_value=True)), \
patch('patroni.postgresql.slots.logger.debug') as mock_debug:
self.s.sync_replication_slots(cluster, self.tags)
mock_debug.assert_called_once()
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
with patch.object(Postgresql, 'is_primary', Mock(return_value=False)), \
patch.object(global_config.__class__, 'is_paused', PropertyMock(return_value=True)), \
patch.object(SlotsHandler, 'drop_replication_slot') as mock_drop:
config.data['slots'].pop('ls')
self.s.sync_replication_slots(cluster, self.tags)
mock_drop.assert_not_called()
self.p.set_role('primary')
with mock.patch('patroni.postgresql.Postgresql.role', new_callable=PropertyMock(return_value='replica')):
self.p.set_role(PostgresqlRole.PRIMARY)
with mock.patch('patroni.postgresql.Postgresql.role',
new_callable=PropertyMock(return_value=PostgresqlRole.REPLICA)):
self.s.sync_replication_slots(cluster, self.tags)
with patch('patroni.dcs.logger.error', new_callable=Mock()) as errorlog_mock:
alias1 = Member(0, 'test-3', 28, {'conn_url': 'postgres://replicator:[email protected]:5436/postgres'})
@@ -79,7 +80,7 @@ class TestSlotsHandler(BaseTestPostgresql):
self.assertTrue("test.3" in ca, "non matching {0}".format(ca))
with patch.object(Postgresql, 'major_version', PropertyMock(return_value=90618)):
self.s.sync_replication_slots(cluster, self.tags)
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.s.sync_replication_slots(cluster, self.tags)
def test_cascading_replica_sync_replication_slots(self):
@@ -91,7 +92,7 @@ class TestSlotsHandler(BaseTestPostgresql):
})
cluster = Cluster(True, config, self.leader, Status(0, {'ls': 10}, []),
[self.me, self.other, self.leadermem, cascading_replica], None, SyncState.empty(), None, None)
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
with patch.object(Postgresql, '_query') as mock_query, \
patch.object(Postgresql, 'is_primary', Mock(return_value=False)):
mock_query.return_value = [('ls', 'logical', 104, 'b', 'a', 5, 12345, 105)]
@@ -147,10 +148,10 @@ class TestSlotsHandler(BaseTestPostgresql):
# sanity for primary
self.p.name = self.leadermem.name
self.assertEqual(
cluster._get_permanent_slots(self.p, self.leadermem, 'primary'),
cluster._get_permanent_slots(self.p, self.leadermem, PostgresqlRole.PRIMARY),
{'foo': {'type': 'logical', 'database': 'a', 'plugin': 'b'}, 'bar': {'type': 'physical'}})
self.assertEqual(
cluster._get_members_slots(self.p.name, 'primary', False, True),
cluster._get_members_slots(self.p.name, PostgresqlRole.PRIMARY, False, True),
{'test_3': {'type': 'physical', 'lsn': 98, 'expected_active': False},
'test_4': {'type': 'physical', 'lsn': 98, 'expected_active': True}})
@@ -158,7 +159,7 @@ class TestSlotsHandler(BaseTestPostgresql):
self.p.name = nostream_node.name
# permanent logical slots are not allowed on nostream node
self.assertEqual(
cluster._get_permanent_slots(self.p, nostream_node, 'replica'),
cluster._get_permanent_slots(self.p, nostream_node, PostgresqlRole.REPLICA),
{'bar': {'type': 'physical'}})
self.assertEqual(
cluster.get_slot_name_on_primary(self.p.name, nostream_node),
@@ -166,7 +167,7 @@ class TestSlotsHandler(BaseTestPostgresql):
# check cascade member-slot existence on nostream node
self.assertEqual(
cluster._get_members_slots(nostream_node.name, 'replica', False, True),
cluster._get_members_slots(nostream_node.name, PostgresqlRole.REPLICA, False, True),
{'leader': {'type': 'physical', 'lsn': 99, 'expected_active': False},
'test_3': {'type': 'physical', 'lsn': 98, 'expected_active': True},
'test_4': {'type': 'physical', 'lsn': 98, 'expected_active': False}})
@@ -174,7 +175,7 @@ class TestSlotsHandler(BaseTestPostgresql):
# cascade also does not entitled to have logical slot on itself ...
self.p.name = cascade_node.name
self.assertEqual(
cluster._get_permanent_slots(self.p, cascade_node, 'replica'),
cluster._get_permanent_slots(self.p, cascade_node, PostgresqlRole.REPLICA),
{'bar': {'type': 'physical'}})
# ... and member-slot on primary
self.assertEqual(
@@ -184,7 +185,7 @@ class TestSlotsHandler(BaseTestPostgresql):
# simple replica must have every permanent slot ...
self.p.name = stream_node.name
self.assertEqual(
cluster._get_permanent_slots(self.p, stream_node, 'replica'),
cluster._get_permanent_slots(self.p, stream_node, PostgresqlRole.REPLICA),
{'foo': {'type': 'logical', 'database': 'a', 'plugin': 'b'}, 'bar': {'type': 'physical'}})
# ... and member-slot on primary
self.assertEqual(
@@ -219,7 +220,7 @@ class TestSlotsHandler(BaseTestPostgresql):
@patch.object(Postgresql, 'is_primary', Mock(return_value=False))
def test__ensure_logical_slots_replica(self):
self.p.set_role('replica')
self.p.set_role(PostgresqlRole.REPLICA)
self.cluster.status.slots['ls'] = 12346
with patch.object(SlotsHandler, 'check_logical_slots_readiness', Mock(return_value=False)):
self.assertEqual(self.s.sync_replication_slots(self.cluster, self.tags), [])
@@ -321,7 +322,7 @@ class TestSlotsHandler(BaseTestPostgresql):
self.assertTrue(mock_query.call_args[0][0].startswith('WITH slots AS (SELECT slot_name, active'))
@patch.object(Postgresql, 'is_primary', Mock(return_value=False))
@patch.object(Postgresql, 'role', PropertyMock(return_value='replica'))
@patch.object(Postgresql, 'role', PropertyMock(return_value=PostgresqlRole.REPLICA))
def test_advance_physical_slots(self):
config = ClusterConfig(1, {'slots': {'blabla': {'type': 'physical'}, 'leader': None}}, 1)
cluster = Cluster(True, config, self.leader, Status(0, {'blabla': 12346}, []),
@@ -363,7 +364,7 @@ class TestSlotsHandler(BaseTestPostgresql):
mock_drop.assert_not_called()
@patch.object(Postgresql, 'is_primary', Mock(return_value=False))
@patch.object(Postgresql, 'role', PropertyMock(return_value='replica'))
@patch.object(Postgresql, 'role', PropertyMock(return_value=PostgresqlRole.REPLICA))
@patch.object(TestTags, 'tags', PropertyMock(return_value={'nofailover': True}))
def test_slots_nofailover_tag(self):
self.p.name = self.leadermem.name