Enable pyright job for every commit (#2675)

And fix remaining issues that the job doesn't fail.
This commit is contained in:
Alexander Kukushkin
2023-05-15 11:38:40 +02:00
committed by GitHub
parent fdcf8b1997
commit 66a0e44371
23 changed files with 337 additions and 280 deletions
+15
View File
@@ -157,3 +157,18 @@ jobs:
steps:
- run: bash <(curl -Ls https://coverage.codacy.com/get.sh) final
if: ${{ env.SECRETS_AVAILABLE == 'true' }}
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies
run: python -m pip install -r requirements.txt psycopg2-binary psycopg
- uses: jakebailey/pyright-action@v1
+29 -24
View File
@@ -24,7 +24,7 @@ from socketserver import ThreadingMixIn
from threading import Thread
from urllib.parse import urlparse, parse_qs
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING, Union
from . import psycopg
from .__main__ import Patroni
@@ -86,6 +86,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
:param client_address: address of the client connection.
:param server: HTTP server that received the request.
"""
if TYPE_CHECKING: # pragma: no cover
assert isinstance(server, RestApiServer)
super(RestApiHandler, self).__init__(request, client_address, server)
self.server: 'RestApiServer' = server
@@ -113,7 +114,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
self.wfile.write('{0} {1} {2}\r\n\r\n'.format(self.protocol_version, status_code, message).encode('utf-8'))
self.log_request(status_code)
def _write_response(self, status_code: int, body: str, content_type: str = 'text/html',
def write_response(self, status_code: int, body: str, content_type: str = 'text/html',
headers: Optional[Dict[str, str]] = None) -> None:
"""Write an HTTP response.
@@ -143,12 +144,12 @@ class RestApiHandler(BaseHTTPRequestHandler):
def _write_json_response(self, status_code: int, response: Any) -> None:
"""Write an HTTP response with a JSON content type.
Call :func:`_write_response` with ``content_type`` as ``application/json``.
Call :func:`write_response` with ``content_type`` as ``application/json``.
:param status_code: response HTTP status code.
:param response: value to be dumped as a JSON string and to be used as the response body.
"""
self._write_response(status_code, json.dumps(response, default=str), content_type='application/json')
self.write_response(status_code, json.dumps(response, default=str), content_type='application/json')
def _write_status_response(self, status_code: int, response: Dict[str, Any]) -> None:
"""Write an HTTP response with Patroni/Postgres status in JSON format.
@@ -457,7 +458,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
* ``patroni_xlog_paused``: ``pg_is_wal_replay_paused()``;
* ``patroni_postgres_server_version``: Postgres version without periods, e.g. ``150002`` for Postgres ``15.2``;
* ``patroni_cluster_unlocked``: ``1`` if no one holds the leader lock, else ``0``;
* ``patroni_failsafe_mode_is_active``: ``1`` if ``failmode`` is currently active, else ``0``;
* ``patroni_failsafe_mode_is_active``: ``1`` if ``failsafe_mode`` is currently active, else ``0``;
* ``patroni_postgres_timeline``: PostgreSQL timeline based on current WAL file name;
* ``patroni_dcs_last_seen``: epoch timestamp when DCS was last contacted successfully;
* ``patroni_pending_restart``: ``1`` if this PostgreSQL node is pending a restart, else ``0``;
@@ -565,7 +566,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
metrics.append("# TYPE patroni_is_paused gauge")
metrics.append("patroni_is_paused{0} {1}".format(scope_label, int(postgres.get('pause', 0))))
self._write_response(200, '\n'.join(metrics) + '\n', content_type='text/plain')
self.write_response(200, '\n'.join(metrics) + '\n', content_type='text/plain')
def _read_json_content(self, body_is_optional: bool = False) -> Optional[Dict[Any, Any]]:
"""Read JSON from HTTP request body.
@@ -615,12 +616,12 @@ class RestApiHandler(BaseHTTPRequestHandler):
request = self._read_json_content()
if request:
cluster = self.server.patroni.dcs.get_cluster(True)
if not (cluster.config and cluster.config.modify_index):
if not (cluster.config and cluster.config.modify_version):
return self.send_error(503)
data = cluster.config.data.copy()
if patch_config(data, request):
value = json.dumps(data, separators=(',', ':'))
if not self.server.patroni.dcs.set_config_value(value, cluster.config.index):
if not self.server.patroni.dcs.set_config_value(value, cluster.config.version):
return self.send_error(409)
self.server.patroni.ha.wakeup()
self._write_json_response(200, data)
@@ -651,7 +652,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
Schedules a reload to Patroni and writes a response with HTTP status `202`.
"""
self.server.patroni.sighup_handler()
self._write_response(202, 'reload scheduled')
self.write_response(202, 'reload scheduled')
def do_GET_failsafe(self) -> None:
"""Handle a ``GET`` request to ``/failsafe`` path.
@@ -684,7 +685,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
if request:
message = self.server.patroni.ha.update_failsafe(request) or 'Accepted'
code = 200 if message == 'Accepted' else 500
self._write_response(code, message)
self.write_response(code, message)
else:
self.send_error(502)
@@ -699,7 +700,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
"""
if os.name == 'nt' and os.getenv('BEHAVE_DEBUG'):
self.server.patroni.api_sigterm()
self._write_response(202, 'shutdown scheduled')
self.write_response(202, 'shutdown scheduled')
@staticmethod
def parse_schedule(schedule: str,
@@ -779,7 +780,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
logger.debug("received restart request: {0}".format(request))
if self.server.patroni.config.get_global_config(cluster).is_paused and 'schedule' in request:
self._write_response(status_code, "Can't schedule restart in the paused state")
self.write_response(status_code, "Can't schedule restart in the paused state")
return
for k in request:
@@ -827,8 +828,9 @@ class RestApiHandler(BaseHTTPRequestHandler):
status_code = 409
# pyright thinks ``data`` can be ``None`` because ``parse_schedule`` call may return ``None``. However, if
# that's the case, ``data`` will be overwritten when the ``for`` loop ends
if TYPE_CHECKING: # pragma: no cover
assert isinstance(data, str)
self._write_response(status_code, data)
self.write_response(status_code, data)
@check_access
def do_DELETE_restart(self) -> None:
@@ -846,7 +848,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
else:
data = "no restarts are scheduled"
code = 404
self._write_response(code, data)
self.write_response(code, data)
@check_access
def do_DELETE_switchover(self) -> None:
@@ -861,7 +863,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
"""
failover = self.server.patroni.dcs.get_cluster().failover
if failover and failover.scheduled_at:
if not self.server.patroni.dcs.manual_failover('', '', index=failover.index):
if not self.server.patroni.dcs.manual_failover('', '', version=failover.version):
return self.send_error(409)
else:
data = "scheduled switchover deleted"
@@ -869,7 +871,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
else:
data = "no switchover is scheduled"
code = 404
self._write_response(code, data)
self.write_response(code, data)
@check_access
def do_POST_reinitialize(self) -> None:
@@ -895,7 +897,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
data = 'reinitialize started'
else:
status_code = 503
self._write_response(status_code, data)
self.write_response(status_code, data)
def poll_failover_result(self, leader: Optional[str], candidate: Optional[str], action: str) -> Tuple[int, str]:
"""Poll failover/switchover operation until it finishes or times out.
@@ -1033,9 +1035,10 @@ class RestApiHandler(BaseHTTPRequestHandler):
status_code = 503
# pyright thinks ``status_code`` can be ``None`` because ``parse_schedule`` call may return ``None``. However,
# if that's the case, ``status_code`` will be overwritten somewhere between ``parse_schedule`` and
# ``_write_response`` calls.
# ``write_response`` calls.
if TYPE_CHECKING: # pragma: no cover
assert isinstance(status_code, int)
self._write_response(status_code, data)
self.write_response(status_code, data)
def do_POST_switchover(self) -> None:
"""Handle a ``POST`` request to ``/switchover`` path.
@@ -1062,7 +1065,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
if patroni.postgresql.citus_handler.is_coordinator() and patroni.ha.is_leader():
cluster = patroni.dcs.get_cluster(True)
patroni.postgresql.citus_handler.handle_event(cluster, request)
self._write_response(200, 'OK')
self.write_response(200, 'OK')
def parse_request(self) -> bool:
"""Override :func:`parse_request` method to enrich basic functionality of :class:`BaseHTTPRequestHandler`.
@@ -1242,7 +1245,7 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
:param patroni: Patroni daemon process.
:param config: ``restapi`` section of Patroni configuration.
"""
self.connection_string = None
self.connection_string: str
self.__auth_key = None
self.__allowlist_include_members: Optional[bool] = None
self.__allowlist: Tuple[Union[IPv4Network, IPv6Network], ...] = ()
@@ -1300,6 +1303,7 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
:returns: ``True`` if *key* matches the password configured for the REST API.
"""
# pyright -- ``__auth_key`` was already checked through the caller method (:func:`check_auth_header`).
if TYPE_CHECKING: # pragma: no cover
assert self.__auth_key is not None
return hmac.compare_digest(self.__auth_key, key.encode('utf-8'))
@@ -1371,16 +1375,16 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
if self.__allowlist or self.__allowlist_include_members:
incoming_ip = ip_address(rh.client_address[0])
if not any(incoming_ip in net for net in self.__allowlist + tuple(self.__members_ips())):
return rh._write_response(403, 'Access is denied')
return rh.write_response(403, 'Access is denied')
if not hasattr(rh.request, 'getpeercert') or not rh.request.getpeercert(): # valid client cert isn't present
if self.__protocol == 'https' and self.__ssl_options.get('verify_client') in ('required', 'optional'):
return rh._write_response(403, 'client certificate required')
return rh.write_response(403, 'client certificate required')
reason = self.check_auth_header(rh.headers.get('Authorization'))
if reason:
headers = {'WWW-Authenticate': 'Basic realm="' + self.patroni.__class__.__name__ + '"'}
return rh._write_response(401, reason, headers=headers)
return rh.write_response(401, reason, headers=headers)
return True
@staticmethod
@@ -1603,6 +1607,7 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
self.__auth_key = base64.b64encode(config['auth'].encode('utf-8')) if 'auth' in config else None
# pyright -- ``__listen`` is initially created as ``None``, but right after that it is replaced with a string
# through :func:`__initialize`.
if TYPE_CHECKING: # pragma: no cover
assert isinstance(self.__listen, str)
self.connection_string = uri(self.__protocol, config.get('connect_address') or self.__listen, 'patroni')
+7 -6
View File
@@ -7,7 +7,7 @@ import yaml
from collections import defaultdict
from copy import deepcopy
from typing import Any, Callable, Collection, Dict, List, Optional, Union
from typing import Any, Callable, Collection, Dict, List, Optional, Union, TYPE_CHECKING
from . import PATRONI_ENV_PREFIX
from .collections import CaseInsensitiveDict
@@ -151,7 +151,7 @@ def get_global_config(cluster: Union[Cluster, None], default: Optional[Dict[str,
:returns: :class:`GlobalConfig` object
"""
# Try to protect from the case when DCS was wiped out
if cluster and cluster.config and cluster.config.modify_index:
if cluster and cluster.config and cluster.config.modify_version:
config = cluster.config.data
else:
config = default or {}
@@ -202,7 +202,7 @@ class Config(object):
def __init__(self, configfile: str,
validator: Optional[Callable[[Dict[str, Any]], List[str]]] = default_validator) -> None:
self._modify_index = -1
self._modify_version = -1
self._dynamic_configuration = {}
self.__environment_configuration = self._build_environment_configuration()
@@ -257,6 +257,7 @@ class Config(object):
def _load_config_file(self) -> Dict[str, Any]:
"""Loads config.yaml from filesystem and applies some values which were set via ENV"""
if TYPE_CHECKING: # pragma: no cover
assert self._config_file is not None
config = self._load_config_path(self._config_file)
patch_config(config, self.__environment_configuration)
@@ -296,9 +297,9 @@ class Config(object):
# configuration could be either ClusterConfig or dict
def set_dynamic_configuration(self, configuration: Union[ClusterConfig, Dict[str, Any]]) -> bool:
if isinstance(configuration, ClusterConfig):
if self._modify_index == configuration.modify_index:
return False # If the index didn't changed there is nothing to do
self._modify_index = configuration.modify_index
if self._modify_version == configuration.modify_version:
return False # If the version didn't changed there is nothing to do
self._modify_version = configuration.modify_version
configuration = configuration.data
if not deep_compare(self._dynamic_configuration, configuration):
+10 -8
View File
@@ -32,9 +32,9 @@ if TYPE_CHECKING: # pragma: no cover
from psycopg2 import cursor
try:
from ydiff import markup_to_pager, PatchStream
from ydiff import markup_to_pager, PatchStream # pyright: ignore [reportMissingModuleSource]
except ImportError: # pragma: no cover
from cdiff import markup_to_pager, PatchStream
from cdiff import markup_to_pager, PatchStream # pyright: ignore [reportMissingModuleSource]
from .dcs import get_dcs as _get_dcs, AbstractDCS, Cluster, Member
from .exceptions import PatroniException
@@ -812,6 +812,7 @@ def _do_failover_or_switchover(obj: Dict[str, Any], action: str, cluster_name: s
r = None
try:
member = cluster.leader.member if cluster.leader else candidate and cluster.get_member(candidate, False)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(member, Member)
r = request_patroni(member, 'post', action, failover_value)
@@ -1052,7 +1053,7 @@ def flush(obj: Dict[str, Any], cluster_name: str, group: Optional[int],
logging.warning('Failing over to DCS')
click.echo('{0} Could not find any accessible member of cluster {1}'.format(timestamp(), cluster_name))
dcs.manual_failover('', '', index=failover.index)
dcs.manual_failover('', '', version=failover.version)
def wait_until_pause_is_applied(dcs: AbstractDCS, paused: bool, old_cluster: Cluster) -> None:
@@ -1060,7 +1061,7 @@ def wait_until_pause_is_applied(dcs: AbstractDCS, paused: bool, old_cluster: Clu
config = get_global_config(old_cluster)
click.echo("'{0}' request sent, waiting until it is recognized by all nodes".format(paused and 'pause' or 'resume'))
old = {m.name: m.index for m in old_cluster.members if m.api_url}
old = {m.name: m.version for m in old_cluster.members if m.api_url}
loop_wait = config.get('loop_wait') or dcs.loop_wait
cluster = None
@@ -1072,7 +1073,7 @@ def wait_until_pause_is_applied(dcs: AbstractDCS, paused: bool, old_cluster: Clu
if TYPE_CHECKING: # pragma: no cover
assert cluster is not None
remaining = [m.name for m in cluster.members if m.data.get('pause', False) != paused
and m.name in old and old[m.name] != m.index]
and m.name in old and old[m.name] != m.version]
if remaining:
return click.echo("{0} members didn't recognized pause state after {1} seconds"
.format(', '.join(remaining), loop_wait))
@@ -1169,7 +1170,7 @@ def show_diff(before_editing: str, after_editing: str) -> None:
(
os.path.basename(p)
for p in (os.environ.get('PAGER'), "less", "more")
if p is not None and shutil.which(p)
if p is not None and bool(shutil.which(p))
),
None,
)
@@ -1347,7 +1348,7 @@ def edit_config(obj: Dict[str, Any], cluster_name: str, group: Optional[int],
return
if force or click.confirm('Apply these changes?'):
if not dcs.set_config_value(json.dumps(changed_data), cluster.config.index):
if not dcs.set_config_value(json.dumps(changed_data), cluster.config.version):
raise PatroniCtlException("Config modification aborted due to concurrent changes")
click.echo("Configuration changed")
@@ -1396,7 +1397,8 @@ def version(obj: Dict[str, Any], cluster_name: str, group: Optional[int], member
@click.pass_obj
def history(obj: Dict[str, Any], cluster_name: str, group: Optional[int], fmt: str) -> None:
cluster = get_dcs(obj, cluster_name, group).get_cluster()
history: List[List[Any]] = list(map(list, cluster.history and cluster.history.lines or []))
cluster_history = cluster.history.lines if cluster.history else []
history: List[List[Any]] = list(map(list, cluster_history))
table_header_row = ['TL', 'LSN', 'Reason', 'Timestamp', 'New Leader']
for line in history:
if len(line) < len(table_header_row):
+46 -44
View File
@@ -126,7 +126,7 @@ _Session = Union[int, float, str, None]
class Member(NamedTuple):
"""Immutable object (namedtuple) which represents single member of PostgreSQL cluster.
Consists of the following fields:
:param index: modification index of a given member key in a Configuration Store
:param version: modification version of a given member key in a Configuration Store
:param name: name of PostgreSQL cluster member
:param session: either session id or just ttl in seconds
:param data: arbitrary data i.e. conn_url, api_url, xlog location, state, role, tags, etc...
@@ -135,18 +135,18 @@ class Member(NamedTuple):
conn_url: connection string containing host, user and password which could be used to access this member.
api_url: REST API url of patroni instance
"""
index: _Version
version: _Version
name: str
session: _Session
data: Dict[str, Any]
@staticmethod
def from_node(index: _Version, name: str, session: _Session, value: str) -> 'Member':
def from_node(version: _Version, name: str, session: _Session, value: str) -> 'Member':
"""
>>> Member.from_node(-1, '', '', '{"conn_url": "postgres://foo@bar/postgres"}') is not None
True
>>> Member.from_node(-1, '', '', '{')
Member(index=-1, name='', session='', data={})
Member(version=-1, name='', session='', data={})
"""
if value.startswith('postgres'):
conn_url, api_url = parse_connection_string(value)
@@ -157,7 +157,7 @@ class Member(NamedTuple):
assert isinstance(data, dict)
except (AssertionError, TypeError, ValueError):
data: Dict[str, Any] = {}
return Member(index, name, session, data)
return Member(version, name, session, data)
@property
def conn_url(self) -> Optional[str]:
@@ -229,7 +229,7 @@ class Member(NamedTuple):
return self.state == 'running'
@property
def version(self) -> Optional[Tuple[int, ...]]:
def patroni_version(self) -> Optional[Tuple[int, ...]]:
version = self.data.get('version')
if version:
try:
@@ -240,7 +240,9 @@ class Member(NamedTuple):
class RemoteMember(Member):
"""Represents a remote member (typically a primary) for a standby cluster"""
def __new__(cls, name: str, data: Dict[str, Any]) -> 'RemoteMember':
@classmethod
def from_name_and_data(cls, name: str, data: Dict[str, Any]) -> 'RemoteMember':
return super(RemoteMember, cls).__new__(cls, -1, name, None, data)
@staticmethod
@@ -261,11 +263,11 @@ class Leader(NamedTuple):
"""Immutable object (namedtuple) which represents leader key.
Consists of the following fields:
:param index: modification index of a leader key in a Configuration Store
:param version: modification version of a leader key in a Configuration Store
:param session: either session id or just ttl in seconds
:param member: reference to a `Member` object which represents current leader (see `Cluster.members`)
"""
index: _Version
version: _Version
session: _Session
member: Member
@@ -294,7 +296,7 @@ class Leader(NamedTuple):
>>> Leader(1, '', Member.from_node(1, '', '', '{"version":"z"}')).checkpoint_after_promote
"""
version = self.member.version
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
@@ -321,13 +323,13 @@ class Failover(NamedTuple):
>>> 'abc' in Failover.from_node(1, 'abc:def')
True
"""
index: _Version
version: _Version
leader: Optional[str]
candidate: Optional[str]
scheduled_at: Optional[datetime.datetime]
@staticmethod
def from_node(index: _Version, value: Union[str, Dict[str, str]]) -> 'Failover':
def from_node(version: _Version, value: Union[str, Dict[str, str]]) -> 'Failover':
if isinstance(value, dict):
data: Dict[str, Any] = value
elif value:
@@ -340,26 +342,26 @@ class Failover(NamedTuple):
t = [a.strip() for a in value.split(':')]
leader = t[0]
candidate = t[1] if len(t) > 1 else None
return Failover(index, leader, candidate, None)
return Failover(version, leader, candidate, None)
else:
data = {}
if data.get('scheduled_at'):
data['scheduled_at'] = dateutil.parser.parse(data['scheduled_at'])
return Failover(index, data.get('leader'), data.get('member'), data.get('scheduled_at'))
return Failover(version, data.get('leader'), data.get('member'), data.get('scheduled_at'))
def __len__(self) -> int:
return int(bool(self.leader)) + int(bool(self.candidate))
class ClusterConfig(NamedTuple):
index: _Version
version: _Version
data: Dict[str, Any]
modify_index: _Version
modify_version: _Version
@staticmethod
def from_node(index: _Version, value: str, modify_index: Optional[_Version] = None) -> 'ClusterConfig':
def from_node(version: _Version, value: str, modify_version: Optional[_Version] = None) -> 'ClusterConfig':
"""
>>> ClusterConfig.from_node(1, '{') is None
False
@@ -370,8 +372,8 @@ class ClusterConfig(NamedTuple):
assert isinstance(data, dict)
except (AssertionError, TypeError, ValueError):
data: Dict[str, Any] = {}
modify_index = 0
return ClusterConfig(index, data, index if modify_index is None else modify_index)
modify_version = 0
return ClusterConfig(version, data, version if modify_version is None else modify_version)
@property
def permanent_slots(self) -> Dict[str, Any]:
@@ -390,16 +392,16 @@ class ClusterConfig(NamedTuple):
class SyncState(NamedTuple):
"""Immutable object (namedtuple) which represents last observed synhcronous replication state
:param index: modification index of a synchronization key in a Configuration Store
:param version: modification version of a synchronization key in a Configuration Store
:param leader: reference to member that was leader
:param sync_standby: synchronous standby list (comma delimited) which are last synchronized to leader
"""
index: Optional[_Version]
version: Optional[_Version]
leader: Optional[str]
sync_standby: Optional[str]
@staticmethod
def from_node(index: Optional[_Version], value: Union[str, Dict[str, Any], None]) -> 'SyncState':
def from_node(version: Optional[_Version], value: Union[str, Dict[str, Any], None]) -> 'SyncState':
"""
>>> SyncState.from_node(1, None).leader is None
True
@@ -418,13 +420,13 @@ class SyncState(NamedTuple):
if value and isinstance(value, str):
value = json.loads(value)
assert isinstance(value, dict)
return SyncState(index, value.get('leader'), value.get('sync_standby'))
return SyncState(version, value.get('leader'), value.get('sync_standby'))
except (AssertionError, TypeError, ValueError):
return SyncState.empty(index)
return SyncState.empty(version)
@staticmethod
def empty(index: Optional[_Version] = None) -> 'SyncState':
return SyncState(index, None, None)
def empty(version: Optional[_Version] = None) -> 'SyncState':
return SyncState(version, None, None)
@property
def is_empty(self) -> bool:
@@ -484,12 +486,12 @@ _HistoryTuple = Union[Tuple[int, int, str], Tuple[int, int, str, str], Tuple[int
class TimelineHistory(NamedTuple):
"""Object representing timeline history file"""
index: _Version
version: _Version
value: Any
lines: List[_HistoryTuple]
@staticmethod
def from_node(index: _Version, value: str) -> 'TimelineHistory':
def from_node(version: _Version, value: str) -> 'TimelineHistory':
"""
>>> h = TimelineHistory.from_node(1, 2)
>>> h.lines
@@ -500,7 +502,7 @@ class TimelineHistory(NamedTuple):
assert isinstance(lines, list)
except (AssertionError, TypeError, ValueError):
lines: List[_HistoryTuple] = []
return TimelineHistory(index, value, lines)
return TimelineHistory(version, value, lines)
class Cluster(NamedTuple):
@@ -537,7 +539,7 @@ class Cluster(NamedTuple):
def is_empty(self):
return self.initialize is None and self.config is None and self.leader is None and self.last_lsn == 0\
and self.members == [] and self.failover is None and self.sync.index is None\
and self.members == [] and self.failover is None and self.sync.version is None\
and self.history is None and self.slots is None and self.failsafe is None and self.workers == {}
def __len__(self) -> int:
@@ -701,7 +703,7 @@ class Cluster(NamedTuple):
@property
def min_version(self) -> Optional[Tuple[int, ...]]:
return next(iter(sorted(m.version for m in self.members if m.version)), None)
return next(iter(sorted(m.patroni_version for m in self.members if m.patroni_version)), None)
class ReturnFalseException(Exception):
@@ -870,6 +872,7 @@ class AbstractDCS(abc.ABC):
if path is None:
path = self.client_path('')
cluster = self._load_cluster(path, self._cluster_loader)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(cluster, Cluster)
return cluster
@@ -887,7 +890,6 @@ class AbstractDCS(abc.ABC):
if isinstance(groups, Cluster): # Zookeeper could return a cached version
cluster = groups
else:
assert isinstance(groups, dict)
cluster = groups.pop(CITUS_COORDINATOR_GROUP_ID, Cluster.empty())
cluster.workers.update(groups)
return cluster
@@ -1006,11 +1008,11 @@ class AbstractDCS(abc.ABC):
process requests (hopefuly temporary), the ~DCSError exception should be raised"""
@abc.abstractmethod
def set_failover_value(self, value: str, index: Optional[Any] = None) -> bool:
def set_failover_value(self, value: str, version: Optional[Any] = None) -> bool:
"""Create or update `/failover` key"""
def manual_failover(self, leader: Optional[str], candidate: Optional[str],
scheduled_at: Optional[datetime.datetime] = None, index: Optional[Any] = None) -> bool:
scheduled_at: Optional[datetime.datetime] = None, version: Optional[Any] = None) -> bool:
failover_value = {}
if leader:
failover_value['leader'] = leader
@@ -1020,10 +1022,10 @@ class AbstractDCS(abc.ABC):
if scheduled_at:
failover_value['scheduled_at'] = scheduled_at.isoformat()
return self.set_failover_value(json.dumps(failover_value, separators=(',', ':')), index)
return self.set_failover_value(json.dumps(failover_value, separators=(',', ':')), version)
@abc.abstractmethod
def set_config_value(self, value: str, index: Optional[Any] = None) -> bool:
def set_config_value(self, value: str, version: Optional[Any] = None) -> bool:
"""Create or update `/config` key"""
@abc.abstractmethod
@@ -1087,16 +1089,16 @@ class AbstractDCS(abc.ABC):
return {'leader': leader, 'sync_standby': ','.join(sorted(sync_standby)) if sync_standby else None}
def write_sync_state(self, leader: Optional[str], sync_standby: Optional[Collection[str]],
index: Optional[Any] = None) -> Optional[SyncState]:
version: Optional[Any] = None) -> Optional[SyncState]:
"""Write the new synchronous state to DCS.
Calls :func:`sync_state` method to build a dict and than calls DCS specific :func:`set_sync_state_value` method.
:param leader: name of the leader node that manages /sync key
:param sync_standby: collection of currently known synchronous standby node names
:param index: for conditional update of the key/object
:param version: for conditional update of the key/object
:returns: the new :class:`SyncState` object or None
"""
sync_value = self.sync_state(leader, sync_standby)
ret = self.set_sync_state_value(json.dumps(sync_value, separators=(',', ':')), index)
ret = self.set_sync_state_value(json.dumps(sync_value, separators=(',', ':')), version)
if not isinstance(ret, bool):
return SyncState.from_node(ret, sync_value)
@@ -1105,23 +1107,23 @@ class AbstractDCS(abc.ABC):
""""""
@abc.abstractmethod
def set_sync_state_value(self, value: str, index: Optional[Any] = None) -> Union[Any, bool]:
def set_sync_state_value(self, value: str, version: Optional[Any] = None) -> Union[Any, bool]:
"""Set synchronous state in DCS, should be implemented in the child class.
:param value: the new value of /sync key
:param index: for conditional update of the key/object
:param version: for conditional update of the key/object
:returns: version of the new object or `False` in case of error
"""
@abc.abstractmethod
def delete_sync_state(self, index: Optional[Any] = None) -> bool:
def delete_sync_state(self, version: Optional[Any] = None) -> bool:
""""""
def watch(self, leader_index: Optional[Any], timeout: float) -> bool:
def watch(self, leader_version: Optional[Any], timeout: float) -> bool:
"""If the current node is a leader it should just sleep.
Any other node should watch for changes of leader key with a given timeout
:param leader_index: index of a leader key
:param leader_version: version of a leader key
:param timeout: timeout in seconds
:returns: `!True` if you would like to reschedule the next run of ha cycle"""
+16 -15
View File
@@ -578,12 +578,12 @@ class Consul(AbstractDCS):
return self.attempt_to_acquire_leader()
@catch_consul_errors
def set_failover_value(self, value: str, index: Optional[int] = None) -> bool:
return self._client.kv.put(self.failover_path, value, cas=index)
def set_failover_value(self, value: str, version: Optional[int] = None) -> bool:
return self._client.kv.put(self.failover_path, value, cas=version)
@catch_consul_errors
def set_config_value(self, value: str, index: Optional[int] = None) -> bool:
return self._client.kv.put(self.config_path, value, cas=index)
def set_config_value(self, value: str, version: Optional[int] = None) -> bool:
return self._client.kv.put(self.config_path, value, cas=version)
@catch_consul_errors
def _write_leader_optime(self, last_lsn: str) -> bool:
@@ -622,7 +622,8 @@ class Consul(AbstractDCS):
raise ConsulError('update_leader timeout')
logger.warning('Recreating the leader key due to session mismatch')
if cluster and cluster.leader:
self._run_and_handle_exceptions(self._client.kv.delete, self.leader_path, cas=cluster.leader.index)
self._run_and_handle_exceptions(self._client.kv.delete, self.leader_path,
cas=cluster.leader.version)
retry.deadline = retry.stoptime - time.time()
if retry.deadline < 0.5:
@@ -653,14 +654,14 @@ class Consul(AbstractDCS):
def _delete_leader(self) -> bool:
cluster = self.cluster
if cluster and isinstance(cluster.leader, Leader) and\
cluster.leader.name == self._name and isinstance(cluster.leader.index, int):
return self._client.kv.delete(self.leader_path, cas=cluster.leader.index)
cluster.leader.name == self._name and isinstance(cluster.leader.version, int):
return self._client.kv.delete(self.leader_path, cas=cluster.leader.version)
return True
@catch_consul_errors
def set_sync_state_value(self, value: str, index: Optional[int] = None) -> Union[int, bool]:
def set_sync_state_value(self, value: str, version: Optional[int] = None) -> Union[int, bool]:
retry = self._retry.copy()
ret = retry(self._client.kv.put, self.sync_path, value, cas=index)
ret = retry(self._client.kv.put, self.sync_path, value, cas=version)
if ret: # We have no other choise, only read after write :(
retry.deadline = retry.stoptime - time.time()
if retry.deadline < 0.5:
@@ -671,21 +672,21 @@ class Consul(AbstractDCS):
return False
@catch_consul_errors
def delete_sync_state(self, index: Optional[int] = None) -> bool:
return self.retry(self._client.kv.delete, self.sync_path, cas=index)
def delete_sync_state(self, version: Optional[int] = None) -> bool:
return self.retry(self._client.kv.delete, self.sync_path, cas=version)
def watch(self, leader_index: Optional[int], timeout: float) -> bool:
def watch(self, leader_version: Optional[int], timeout: float) -> bool:
self._last_session_refresh = 0
if self.__do_not_watch:
self.__do_not_watch = False
return True
if leader_index:
if leader_version:
end_time = time.time() + timeout
while timeout >= 1:
try:
idx, _ = self._client.kv.get(self.leader_path, index=leader_index, wait=str(timeout) + 's')
return str(idx) != str(leader_index)
idx, _ = self._client.kv.get(self.leader_path, index=leader_version, wait=str(timeout) + 's')
return str(idx) != str(leader_version)
except (ConsulException, HTTPException, HTTPError, socket.error, socket.timeout):
logger.exception('watch')
+28 -23
View File
@@ -290,6 +290,7 @@ class AbstractEtcdClientWithFailover(abc.ABC, etcd.Client):
etcd_nodes = len(machines_cache)
except Exception as e:
logger.debug('Failed to update list of etcd nodes: %r', e)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(retry, Retry) # etcd.EtcdConnectionFailed is raised only if retry is not None!
sleeptime = retry.sleeptime
remaining_time = retry.stoptime - sleeptime - time.time()
@@ -502,6 +503,17 @@ class AbstractEtcd(AbstractDCS):
if isinstance(raise_ex, Exception):
raise raise_ex
def handle_etcd_exceptions(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
try:
retval = func(self, *args, **kwargs)
self._has_failed = False
return retval
except (RetryFailedError, etcd.EtcdException) as e:
self._handle_exception(e)
return False
except Exception as e:
self._handle_exception(e, raise_ex=self._client.ERROR_CLS('unexpected error'))
def _run_and_handle_exceptions(self, method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
retry = kwargs.pop('retry', self.retry)
try:
@@ -624,16 +636,7 @@ class AbstractEtcd(AbstractDCS):
def catch_etcd_errors(func: Callable[..., Any]) -> Any:
def wrapper(self: AbstractEtcd, *args: Any, **kwargs: Any) -> Any:
try:
retval = func(self, *args, **kwargs)
self._has_failed = False
return retval
except (RetryFailedError, etcd.EtcdException) as e:
self._handle_exception(e)
return False
except Exception as e:
self._handle_exception(e, raise_ex=self._client.ERROR_CLS('unexpected error'))
return self.handle_etcd_exceptions(func, *args, **kwargs)
return wrapper
@@ -645,6 +648,7 @@ class Etcd(AbstractEtcd):
@property
def _client(self) -> EtcdClient:
if TYPE_CHECKING: # pragma: no cover
assert isinstance(self._abstract_client, EtcdClient)
return self._abstract_client
@@ -696,8 +700,8 @@ class Etcd(AbstractEtcd):
if leader:
member = Member(-1, leader.value, None, {})
member = ([m for m in members if m.name == leader.value] or [member])[0]
index = etcd_index if etcd_index > leader.modifiedIndex else leader.modifiedIndex + 1
leader = Leader(index, leader.ttl, member)
version = etcd_index if etcd_index > leader.modifiedIndex else leader.modifiedIndex + 1
leader = Leader(version, leader.ttl, member)
# failover key
failover = nodes.get(self._FAILOVER)
@@ -742,6 +746,7 @@ class Etcd(AbstractEtcd):
except Exception as e:
self._handle_exception(e, 'get_cluster', raise_ex=EtcdError('Etcd is not responding properly'))
self._has_failed = False
if TYPE_CHECKING: # pragma: no cover
assert cluster is not None
return cluster
@@ -766,12 +771,12 @@ class Etcd(AbstractEtcd):
return self._run_and_handle_exceptions(self._do_attempt_to_acquire_leader, retry=None)
@catch_etcd_errors
def set_failover_value(self, value: str, index: Optional[int] = None) -> bool:
return bool(self._client.write(self.failover_path, value, prevIndex=index or 0))
def set_failover_value(self, value: str, version: Optional[int] = None) -> bool:
return bool(self._client.write(self.failover_path, value, prevIndex=version or 0))
@catch_etcd_errors
def set_config_value(self, value: str, index: Optional[int] = None) -> bool:
return bool(self._client.write(self.config_path, value, prevIndex=index or 0))
def set_config_value(self, value: str, version: Optional[int] = None) -> bool:
return bool(self._client.write(self.config_path, value, prevIndex=version or 0))
@catch_etcd_errors
def _write_leader_optime(self, last_lsn: str) -> bool:
@@ -817,24 +822,24 @@ class Etcd(AbstractEtcd):
return bool(self._client.write(self.history_path, value))
@catch_etcd_errors
def set_sync_state_value(self, value: str, index: Optional[int] = None) -> Union[int, bool]:
return self.retry(self._client.write, self.sync_path, value, prevIndex=index or 0).modifiedIndex
def set_sync_state_value(self, value: str, version: Optional[int] = None) -> Union[int, bool]:
return self.retry(self._client.write, self.sync_path, value, prevIndex=version or 0).modifiedIndex
@catch_etcd_errors
def delete_sync_state(self, index: Optional[int] = None) -> bool:
return bool(self.retry(self._client.delete, self.sync_path, prevIndex=index or 0))
def delete_sync_state(self, version: Optional[int] = None) -> bool:
return bool(self.retry(self._client.delete, self.sync_path, prevIndex=version or 0))
def watch(self, leader_index: Optional[int], timeout: float) -> bool:
def watch(self, leader_version: Optional[int], timeout: float) -> bool:
if self.__do_not_watch:
self.__do_not_watch = False
return True
if leader_index:
if leader_version:
end_time = time.time() + timeout
while timeout >= 1: # when timeout is too small urllib3 doesn't have enough time to connect
try:
result = self._client.watch(self.leader_path, index=leader_index, timeout=timeout + 0.5)
result = self._client.watch(self.leader_path, index=leader_version, timeout=timeout + 0.5)
self._has_failed = False
if result.action == 'compareAndSwap':
time.sleep(0.01)
+43 -37
View File
@@ -13,7 +13,7 @@ from collections import defaultdict
from enum import IntEnum
from urllib3.exceptions import ReadTimeoutError, ProtocolError
from threading import Condition, Lock, Thread
from typing import Any, Callable, Collection, Dict, Iterator, List, Optional, Tuple, Type, Union
from typing import Any, Callable, Collection, Dict, Iterator, List, Optional, Tuple, Type, TYPE_CHECKING, Union
from . import ClusterConfig, Cluster, Failover, Leader, Member, SyncState,\
TimelineHistory, ReturnFalseException, catch_return_false_exception, citus_group_re
@@ -145,6 +145,7 @@ errCodeToClientError = {getattr(s, 'code'): s for s in Etcd3ClientError.__subcla
def _raise_for_data(data: Union[bytes, str, Dict[str, Union[Any, Dict[str, Any]]]],
status_code: Optional[int] = None) -> Etcd3ClientError:
try:
if TYPE_CHECKING: # pragma: no cover
assert isinstance(data, dict)
data_error: Optional[Dict[str, Any]] = data.get('error') or data.get('Error')
if isinstance(data_error, dict): # streaming response
@@ -153,6 +154,7 @@ def _raise_for_data(data: Union[bytes, str, Dict[str, Union[Any, Dict[str, Any]]
error: str = data_error['message']
else:
data_code = data.get('code') or data.get('Code')
if TYPE_CHECKING: # pragma: no cover
assert not isinstance(data_code, dict)
code = data_code
error = str(data_error)
@@ -193,28 +195,7 @@ def build_range_request(key: str, range_end: Union[bytes, str, None] = None) ->
def _handle_auth_errors(func: Callable[..., Any]) -> Any:
def wrapper(self: 'Etcd3Client', *args: Any, **kwargs: Any) -> Any:
def retry(ex: Exception) -> Any:
if self.username and self.password:
self.authenticate()
return func(self, *args, **kwargs)
else:
logger.fatal('Username or password not set, authentication is not possible')
raise ex
try:
return func(self, *args, **kwargs)
except (UserEmpty, PermissionDenied) as e: # no token provided
# PermissionDenied is raised on 3.0 and 3.1
if self._cluster_version < (3, 3) and (not isinstance(e, PermissionDenied)
or self._cluster_version < (3, 2)):
raise UnsupportedEtcdVersion('Authentication is required by Etcd cluster but not '
'supported on version lower than 3.3.0. Cluster version: '
'{0}'.format('.'.join(map(str, self._cluster_version))))
return retry(e)
except InvalidAuthToken as e:
logger.error('Invalid auth token: %s', self._token)
return retry(e)
return self.handle_auth_errors(func, *args, **kwargs)
return wrapper
@@ -322,6 +303,29 @@ class Etcd3Client(AbstractEtcdClientWithFailover):
self._token = response.get('token')
return old_token != self._token
def handle_auth_errors(self: 'Etcd3Client', func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
def retry(ex: Exception) -> Any:
if self.username and self.password:
self.authenticate()
return func(self, *args, **kwargs)
else:
logger.fatal('Username or password not set, authentication is not possible')
raise ex
try:
return func(self, *args, **kwargs)
except (UserEmpty, PermissionDenied) as e: # no token provided
# PermissionDenied is raised on 3.0 and 3.1
if self._cluster_version < (3, 3) and (not isinstance(e, PermissionDenied)
or self._cluster_version < (3, 2)):
raise UnsupportedEtcdVersion('Authentication is required by Etcd cluster but not '
'supported on version lower than 3.3.0. Cluster version: '
'{0}'.format('.'.join(map(str, self._cluster_version))))
return retry(e)
except InvalidAuthToken as e:
logger.error('Invalid auth token: %s', self._token)
return retry(e)
@_handle_auth_errors
def range(self, key: str, range_end: Union[bytes, str, None] = None,
retry: Optional[Retry] = None) -> Dict[str, Any]:
@@ -401,7 +405,7 @@ class KVCache(Thread):
self._leader_key = base64_encode(dcs.leader_path)
self._optime_key = base64_encode(dcs.leader_optime_path)
self._status_key = base64_encode(dcs.status_path)
self._name = base64_encode(dcs._name)
self._name = base64_encode(getattr(dcs, '_name')) # pyright
self._is_ready = False
self._response = None
self._response_lock = Lock()
@@ -582,9 +586,9 @@ class PatroniEtcd3Client(Etcd3Client):
self._kv_cache.condition.wait(timeout)
def get_cluster(self, path: str) -> List[Dict[str, Any]]:
if self._kv_cache and self._etcd3._retry.deadline is not None and path.startswith(self._etcd3.cluster_prefix):
if self._kv_cache and path.startswith(self._etcd3.cluster_prefix):
with self._kv_cache.condition:
self._wait_cache(self._etcd3._retry.deadline)
self._wait_cache(self.read_timeout)
ret = self._kv_cache.copy()
else:
ret = self._etcd3.retry(self.prefix, path).get('kvs', [])
@@ -621,7 +625,6 @@ class Etcd3(AbstractEtcd):
def __init__(self, config: Dict[str, Any]) -> None:
super(Etcd3, self).__init__(config, PatroniEtcd3Client, (DeadlineExceeded, Unavailable, FailedPrecondition))
assert isinstance(self._client, PatroniEtcd3Client)
self.__do_not_watch = False
self._lease = None
self._last_lease_refresh = 0
@@ -633,11 +636,13 @@ class Etcd3(AbstractEtcd):
@property
def _client(self) -> PatroniEtcd3Client:
if TYPE_CHECKING: # pragma: no cover
assert isinstance(self._abstract_client, PatroniEtcd3Client)
return self._abstract_client
def set_socket_options(self, sock: socket.socket,
socket_options: Optional[Collection[Tuple[int, int, int]]]) -> None:
if TYPE_CHECKING: # pragma: no cover
assert self._retry.deadline is not None
enable_keepalive(sock, self.ttl, int(self.loop_wait + self._retry.deadline))
@@ -773,6 +778,7 @@ class Etcd3(AbstractEtcd):
except Exception as e:
self._handle_exception(e, 'get_cluster', raise_ex=Etcd3Error('Etcd is not responding properly'))
self._has_failed = False
if TYPE_CHECKING: # pragma: no cover
assert cluster is not None
return cluster
@@ -841,12 +847,12 @@ class Etcd3(AbstractEtcd):
return ret
@catch_etcd_errors
def set_failover_value(self, value: str, index: Optional[str] = None) -> bool:
return bool(self._client.put(self.failover_path, value, mod_revision=index))
def set_failover_value(self, value: str, version: Optional[str] = None) -> bool:
return bool(self._client.put(self.failover_path, value, mod_revision=version))
@catch_etcd_errors
def set_config_value(self, value: str, index: Optional[str] = None) -> bool:
return bool(self._client.put(self.config_path, value, mod_revision=index))
def set_config_value(self, value: str, version: Optional[str] = None) -> bool:
return bool(self._client.put(self.config_path, value, mod_revision=version))
@catch_etcd_errors
def _write_leader_optime(self, last_lsn: str) -> bool:
@@ -893,7 +899,7 @@ class Etcd3(AbstractEtcd):
def _delete_leader(self) -> bool:
cluster = self.cluster
if cluster and isinstance(cluster.leader, Leader) and cluster.leader.name == self._name:
return self._client.deleterange(self.leader_path, mod_revision=cluster.leader.index)
return self._client.deleterange(self.leader_path, mod_revision=cluster.leader.version)
return True
@catch_etcd_errors
@@ -909,15 +915,15 @@ class Etcd3(AbstractEtcd):
return bool(self._client.put(self.history_path, value))
@catch_etcd_errors
def set_sync_state_value(self, value: str, index: Optional[str] = None) -> Union[str, bool]:
return self.retry(self._client.put, self.sync_path, value, mod_revision=index)\
def set_sync_state_value(self, value: str, version: Optional[str] = None) -> Union[str, bool]:
return self.retry(self._client.put, self.sync_path, value, mod_revision=version)\
.get('header', {}).get('revision', False)
@catch_etcd_errors
def delete_sync_state(self, index: Optional[str] = None) -> bool:
return self.retry(self._client.deleterange, self.sync_path, mod_revision=index)
def delete_sync_state(self, version: Optional[str] = None) -> bool:
return self.retry(self._client.deleterange, self.sync_path, mod_revision=version)
def watch(self, leader_index: Optional[str], timeout: float) -> bool:
def watch(self, leader_version: Optional[str], timeout: float) -> bool:
if self.__do_not_watch:
self.__do_not_watch = False
return True
+41 -31
View File
@@ -135,10 +135,13 @@ class K8sConfig(object):
context = context or config['current-context']
context_value = self._get_by_name(config, 'context', context)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(context_value, dict)
cluster = self._get_by_name(config, 'cluster', context_value['cluster'])
if TYPE_CHECKING: # pragma: no cover
assert isinstance(cluster, dict)
user = self._get_by_name(config, 'user', context_value['user'])
if TYPE_CHECKING: # pragma: no cover
assert isinstance(user, dict)
self._server = cluster['server'].rstrip('/')
@@ -281,6 +284,7 @@ class K8sClient(object):
try:
response = self.pool_manager.request('GET', base_uri + path, **kwargs)
endpoint = self._handle_server_response(response, True)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(endpoint, K8sObject)
for subset in endpoint.subsets:
for port in subset.ports:
@@ -412,6 +416,7 @@ class K8sClient(object):
except Exception as e:
logger.debug('Failed to update list of K8s master nodes: %r', e)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(retry, Retry) # K8sConnectionFailed is raised only if retry is not None!
sleeptime = retry.sleeptime
remaining_time = (retry.stoptime or time.time()) - sleeptime - time.time()
@@ -559,10 +564,23 @@ class CoreV1ApiProxy(object):
return self._use_endpoints
def _run_and_handle_exceptions(method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
try:
return method(*args, **kwargs)
except k8s_client.rest.ApiException as e:
if e.status == 403:
logger.exception('Permission denied')
elif e.status != 409: # Object exists or conflict in resource_version
logger.exception('Unexpected error from Kubernetes API')
return False
except (RetryFailedError, K8sException) as e:
raise KubernetesError(e)
def catch_kubernetes_errors(func: Callable[..., Any]) -> Callable[..., Any]:
def wrapper(self: 'Kubernetes', *args: Any, **kwargs: Any) -> Any:
try:
return self._run_and_handle_exceptions(func, self, *args, **kwargs)
return _run_and_handle_exceptions(func, self, *args, **kwargs)
except KubernetesError:
return False
return wrapper
@@ -584,7 +602,8 @@ class ObjectCache(Thread):
self._response_lock = Lock() # protect the `self._response` from concurrent access
self._object_cache: Dict[str, K8sObject] = {}
self._object_cache_lock = Lock()
self._annotations_map = {self._dcs.leader_path: self._dcs._LEADER, self._dcs.config_path: self._dcs._CONFIG}
self._annotations_map = {self._dcs.leader_path: getattr(self._dcs, '_LEADER'),
self._dcs.config_path: getattr(self._dcs, '_CONFIG')} # pyright
self.start()
def _list(self) -> K8sObject:
@@ -779,19 +798,6 @@ class Kubernetes(AbstractDCS):
kwargs['_retry'] = retry
return retry(method, *args, **kwargs)
@staticmethod
def _run_and_handle_exceptions(method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
try:
return method(*args, **kwargs)
except k8s_client.rest.ApiException as e:
if e.status == 403:
logger.exception('Permission denied')
elif e.status != 409: # Object exists or conflict in resource_version
logger.exception('Unexpected error from Kubernetes API')
return False
except (RetryFailedError, K8sException) as e:
raise KubernetesError(e)
def client_path(self, path: str) -> str:
return super(Kubernetes, self).client_path(path)[1:].replace('/', '-')
@@ -818,6 +824,7 @@ class Kubernetes(AbstractDCS):
Either cause by changes in the local configuration file + SIGHUP or by changes of dynamic configuration"""
super(Kubernetes, self).reload_config(config)
if TYPE_CHECKING: # pragma: no cover
assert self._retry.deadline is not None
self._api.configure_timeouts(self.loop_wait, self._retry.deadline, self.ttl)
@@ -954,6 +961,7 @@ class Kubernetes(AbstractDCS):
def __load_cluster(
self, group: Optional[str], loader: Callable[[Dict[str, Any]], Union[Cluster, Dict[int, Cluster]]]
) -> Union[Cluster, Dict[int, Cluster]]:
if TYPE_CHECKING: # pragma: no cover
assert self._retry.deadline is not None
stop_time = time.time() + self._retry.deadline
self._api.refresh_api_servers_cache()
@@ -978,6 +986,7 @@ class Kubernetes(AbstractDCS):
def get_citus_coordinator(self) -> Optional[Cluster]:
try:
ret = self.__load_cluster(str(CITUS_COORDINATOR_GROUP_ID), self._cluster_loader)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(ret, Cluster)
return ret
except Exception as e:
@@ -1170,7 +1179,7 @@ class Kubernetes(AbstractDCS):
if kind and (kind_annotations.get(self._LEADER) != self._name or kind_resource_version == resource_version):
return False
return bool(self._run_and_handle_exceptions(self._patch_or_create, self.leader_path, annotations,
return bool(_run_and_handle_exceptions(self._patch_or_create, self.leader_path, annotations,
kind_resource_version, ips=ips, retry=_retry))
def update_leader(self, last_lsn: Optional[int], slots: Optional[Dict[str, int]] = None,
@@ -1232,24 +1241,24 @@ class Kubernetes(AbstractDCS):
def take_leader(self) -> bool:
return self.attempt_to_acquire_leader()
def set_failover_value(self, value: str, index: Optional[str] = None) -> bool:
def set_failover_value(self, value: str, version: Optional[str] = None) -> bool:
"""Unused"""
raise NotImplementedError # pragma: no cover
def manual_failover(self, leader: Optional[str], candidate: Optional[str],
scheduled_at: Optional[datetime.datetime] = None, index: Optional[str] = None) -> bool:
scheduled_at: Optional[datetime.datetime] = None, version: Optional[str] = None) -> bool:
annotations = {'leader': leader or None, 'member': candidate or None,
'scheduled_at': scheduled_at and scheduled_at.isoformat()}
patch = bool(self.cluster and isinstance(self.cluster.failover, Failover) and self.cluster.failover.index)
return bool(self.patch_or_create(self.failover_path, annotations, index, bool(index or patch), False))
patch = bool(self.cluster and isinstance(self.cluster.failover, Failover) and self.cluster.failover.version)
return bool(self.patch_or_create(self.failover_path, annotations, version, bool(version or patch), False))
@property
def _config_resource_version(self) -> Optional[str]:
config = self._kinds.get(self.config_path)
return config and config.metadata.resource_version
def set_config_value(self, value: str, index: Optional[str] = None) -> bool:
return self.patch_or_create_config({self._CONFIG: value}, index, bool(self._config_resource_version), False)
def set_config_value(self, value: str, version: Optional[str] = None) -> bool:
return self.patch_or_create_config({self._CONFIG: value}, version, bool(self._config_resource_version), False)
@catch_kubernetes_errors
def touch_member(self, data: Dict[str, Any]) -> bool:
@@ -1279,7 +1288,8 @@ class Kubernetes(AbstractDCS):
def initialize(self, create_new: bool = True, sysid: str = "") -> bool:
cluster = self.cluster
resource_version = str(cluster.config.index) if cluster and cluster.config and cluster.config.index else None
resource_version = str(cluster.config.version)\
if cluster and cluster.config and cluster.config.version else None
return self.patch_or_create_config({self._INITIALIZE: sysid}, resource_version)
def _delete_leader(self) -> bool:
@@ -1308,34 +1318,34 @@ class Kubernetes(AbstractDCS):
def set_history_value(self, value: str) -> bool:
return self.patch_or_create_config({self._HISTORY: value}, None, bool(self._config_resource_version), False)
def set_sync_state_value(self, value: str, index: Optional[str] = None) -> bool:
def set_sync_state_value(self, value: str, version: Optional[str] = None) -> bool:
"""Unused"""
raise NotImplementedError # pragma: no cover
def write_sync_state(self, leader: Optional[str], sync_standby: Optional[Collection[str]],
index: Optional[str] = None) -> Optional[SyncState]:
version: Optional[str] = None) -> Optional[SyncState]:
"""Prepare and write annotations to $SCOPE-sync Endpoint or ConfigMap.
:param leader: name of the leader node that manages /sync key
:param sync_standby: collection of currently known synchronous standby node names
:param index: last known `resource_version` for conditional update of the object
:param version: last known `resource_version` for conditional update of the object
:returns: the new :class:`SyncState` object or None
"""
sync_state = self.sync_state(leader, sync_standby)
ret = self.patch_or_create(self.sync_path, sync_state, index, False)
ret = self.patch_or_create(self.sync_path, sync_state, version, False)
if not isinstance(ret, bool):
return SyncState.from_node(ret.metadata.resource_version, sync_state)
def delete_sync_state(self, index: Optional[str] = None) -> bool:
def delete_sync_state(self, version: Optional[str] = None) -> bool:
"""Patch annotations of $SCOPE-sync Endpoint or ConfigMap with empty values.
Effectively it removes "leader" and "sync_standby" annotations from the object.
:param index: last known `resource_version` for conditional update of the object
:param version: last known `resource_version` for conditional update of the object
:returns: `True` if "delete" was successful
"""
return self.write_sync_state(None, None, index=index) is not None
return self.write_sync_state(None, None, version=version) is not None
def watch(self, leader_index: Optional[str], timeout: float) -> bool:
def watch(self, leader_version: Optional[str], timeout: float) -> bool:
if self.__do_not_watch:
self.__do_not_watch = False
return True
+10 -10
View File
@@ -430,11 +430,11 @@ class Raft(AbstractDCS):
return self._sync_obj.set(self.leader_path, self._name, ttl=self._ttl,
handle_raft_error=False, prevExist=False) is not False
def set_failover_value(self, value: str, index: Optional[int] = None) -> bool:
return self._sync_obj.set(self.failover_path, value, prevIndex=index) is not False
def set_failover_value(self, value: str, version: Optional[int] = None) -> bool:
return self._sync_obj.set(self.failover_path, value, prevIndex=version) is not False
def set_config_value(self, value: str, index: Optional[int] = None) -> bool:
return self._sync_obj.set(self.config_path, value, prevIndex=index) is not False
def set_config_value(self, value: str, version: Optional[int] = None) -> bool:
return self._sync_obj.set(self.config_path, value, prevIndex=version) is not False
def touch_member(self, data: Dict[str, Any]) -> bool:
value = json.dumps(data, separators=(',', ':'))
@@ -458,17 +458,17 @@ class Raft(AbstractDCS):
def set_history_value(self, value: str) -> bool:
return self._sync_obj.set(self.history_path, value) is not False
def set_sync_state_value(self, value: str, index: Optional[int] = None) -> Union[int, bool]:
ret = self._sync_obj.set(self.sync_path, value, prevIndex=index)
def set_sync_state_value(self, value: str, version: Optional[int] = None) -> Union[int, bool]:
ret = self._sync_obj.set(self.sync_path, value, prevIndex=version)
if isinstance(ret, dict):
return ret['index']
return ret
def delete_sync_state(self, index: Optional[int] = None) -> bool:
return self._sync_obj.delete(self.sync_path, prevIndex=index)
def delete_sync_state(self, version: Optional[int] = None) -> bool:
return self._sync_obj.delete(self.sync_path, prevIndex=version)
def watch(self, leader_index: Optional[int], timeout: float) -> bool:
def watch(self, leader_version: Optional[int], timeout: float) -> bool:
try:
return super(Raft, self).watch(leader_index, timeout)
return super(Raft, self).watch(leader_version, timeout)
finally:
self.event.clear()
+15 -15
View File
@@ -273,7 +273,7 @@ class ZooKeeper(AbstractDCS):
member = Member(-1, leader[0], None, {})
member = ([m for m in members if m.name == leader[0]] or [member])[0]
leader = Leader(leader[1].version, leader[1].ephemeralOwner, member)
self._fetch_cluster = member.index == -1
self._fetch_cluster = member.version == -1
# get last known leader lsn and slots
last_lsn, slots = self.get_status(path, leader)
@@ -357,19 +357,19 @@ class ZooKeeper(AbstractDCS):
logger.info('Could not take out TTL lock')
return False
def _set_or_create(self, key: str, value: str, index: Optional[int] = None,
def _set_or_create(self, key: str, value: str, version: Optional[int] = None,
retry: bool = False, do_not_create_empty: bool = False) -> Union[int, bool]:
value_bytes = value.encode('utf-8')
try:
if retry:
ret = self._client.retry(self._client.set, key, value_bytes, version=index or -1)
ret = self._client.retry(self._client.set, key, value_bytes, version=version or -1)
else:
ret = self._client.set_async(key, value_bytes, version=index or -1).get(timeout=1)
ret = self._client.set_async(key, value_bytes, version=version or -1).get(timeout=1)
return ret.version
except NoNodeError:
if do_not_create_empty and not value_bytes:
return True
elif index is None:
elif version is None:
if self._create(key, value_bytes, retry):
return 0
else:
@@ -378,11 +378,11 @@ class ZooKeeper(AbstractDCS):
logger.exception('Failed to update %s', key)
return False
def set_failover_value(self, value: str, index: Optional[int] = None) -> bool:
return self._set_or_create(self.failover_path, value, index) is not False
def set_failover_value(self, value: str, version: Optional[int] = None) -> bool:
return self._set_or_create(self.failover_path, value, version) is not False
def set_config_value(self, value: str, index: Optional[int] = None) -> bool:
return self._set_or_create(self.config_path, value, index, retry=True) is not False
def set_config_value(self, value: str, version: Optional[int] = None) -> bool:
return self._set_or_create(self.config_path, value, version, retry=True) is not False
def initialize(self, create_new: bool = True, sysid: str = "") -> bool:
sysid_bytes = sysid.encode('utf-8')
@@ -494,14 +494,14 @@ class ZooKeeper(AbstractDCS):
def set_history_value(self, value: str) -> bool:
return self._set_or_create(self.history_path, value) is not False
def set_sync_state_value(self, value: str, index: Optional[int] = None) -> Union[int, bool]:
return self._set_or_create(self.sync_path, value, index, retry=True, do_not_create_empty=True)
def set_sync_state_value(self, value: str, version: Optional[int] = None) -> Union[int, bool]:
return self._set_or_create(self.sync_path, value, version, retry=True, do_not_create_empty=True)
def delete_sync_state(self, index: Optional[int] = None) -> bool:
return self.set_sync_state_value("{}", index) is not False
def delete_sync_state(self, version: Optional[int] = None) -> bool:
return self.set_sync_state_value("{}", version) is not False
def watch(self, leader_index: Optional[int], timeout: float) -> bool:
ret = super(ZooKeeper, self).watch(leader_index, timeout + 0.5)
def watch(self, leader_version: Optional[int], timeout: float) -> bool:
ret = super(ZooKeeper, self).watch(leader_version, timeout + 0.5)
if ret and not self._fetch_status:
self._fetch_cluster = True
return ret or self._fetch_cluster
+18 -18
View File
@@ -100,8 +100,8 @@ class Failsafe(object):
@property
def leader(self) -> Optional[Leader]:
with self._lock:
if self._last_update + self._dcs.ttl > time.time():
return Leader('', '', RemoteMember(self._name, {'api_url': self._api_url,
if self._last_update + self._dcs.ttl > time.time() and self._name:
return Leader('', '', RemoteMember.from_name_and_data(self._name, {'api_url': self._api_url,
'conn_url': self._conn_url,
'slots': self._slots}))
@@ -596,7 +596,7 @@ class Ha(object):
if sync_common != current:
logger.info("Updating synchronous privilege temporarily from %s to %s",
list(current), list(sync_common))
sync = self.dcs.write_sync_state(self.state_handler.name, sync_common, index=sync.index)
sync = self.dcs.write_sync_state(self.state_handler.name, sync_common, version=sync.version)
if not sync:
return logger.info('Synchronous replication key updated by someone else.')
@@ -614,11 +614,11 @@ class Ha(object):
time.sleep(2)
_, allow_promote = self.state_handler.sync_handler.current_state(self.cluster)
if allow_promote and allow_promote != sync_common:
if not self.dcs.write_sync_state(self.state_handler.name, allow_promote, index=sync.index):
if not self.dcs.write_sync_state(self.state_handler.name, allow_promote, version=sync.version):
return logger.info("Synchronous replication key updated by someone else")
logger.info("Synchronous standby status assigned to %s", list(allow_promote))
else:
if not self.cluster.sync.is_empty and self.dcs.delete_sync_state(index=self.cluster.sync.index):
if not self.cluster.sync.is_empty and self.dcs.delete_sync_state(version=self.cluster.sync.version):
logger.info("Disabled synchronous replication")
self.state_handler.sync_handler.set_synchronous_standby_names(CaseInsensitiveSet())
@@ -727,7 +727,7 @@ class Ha(object):
if self.is_synchronous_mode():
# Just set ourselves as the authoritative source of truth for now. We don't want to wait for standbys
# to connect. We will try finding a synchronous standby in the next cycle.
if not self.dcs.write_sync_state(self.state_handler.name, None, index=self.cluster.sync.index):
if not self.dcs.write_sync_state(self.state_handler.name, None, version=self.cluster.sync.version):
# 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'
@@ -800,9 +800,8 @@ class Ha(object):
data['slots'] = self.state_handler.slots()
except Exception:
logger.exception('Exception when called state_handler.slots()')
members = [RemoteMember(name, {'api_url': url})
for name, url in failsafe.items()
if name != self.state_handler.name]
members = [RemoteMember.from_name_and_data(name, {'api_url': url})
for name, url in failsafe.items() if name != self.state_handler.name]
if not members: # A sinlge node cluster
return True
pool = ThreadPool(len(members))
@@ -907,7 +906,7 @@ class Ha(object):
if not self.cluster.get_member(failover.candidate, fallback_to_leader=False)\
and self.state_handler.is_leader():
logger.warning("manual failover: removing failover key because failover candidate is not running")
self.dcs.manual_failover('', '', index=failover.index)
self.dcs.manual_failover('', '', version=failover.version)
return None
return False
@@ -998,7 +997,8 @@ class Ha(object):
if failsafe_members and self.state_handler.name not in failsafe_members:
return False
# Race among not only existing cluster members, but also all known members from the failsafe config
all_known_members += [RemoteMember(name, {'api_url': url}) for name, url in failsafe_members.items()]
all_known_members += [RemoteMember.from_name_and_data(name, {'api_url': url})
for name, url in failsafe_members.items()]
all_known_members += self.cluster.members
# When in sync mode, only last known primary and sync standby are allowed to promote automatically.
@@ -1148,7 +1148,7 @@ class Ha(object):
if (failover.scheduled_at and not
self.should_run_scheduled_action("failover", failover.scheduled_at, lambda:
self.dcs.manual_failover('', '', index=failover.index))):
self.dcs.manual_failover('', '', version=failover.version))):
return
if not failover.leader or failover.leader == self.state_handler.name:
@@ -1178,7 +1178,7 @@ class Ha(object):
failover.leader, self.state_handler.name)
logger.info('Cleaning up failover key')
self.dcs.manual_failover('', '', index=failover.index)
self.dcs.manual_failover('', '', version=failover.version)
def process_unhealthy_cluster(self) -> str:
"""Cluster has no leader key"""
@@ -1189,7 +1189,7 @@ class Ha(object):
if failover:
if self.is_paused() and failover.leader and failover.candidate:
logger.info('Updating failover key after acquiring leader lock...')
self.dcs.manual_failover('', failover.candidate, failover.scheduled_at, failover.index)
self.dcs.manual_failover('', failover.candidate, failover.scheduled_at, failover.version)
else:
logger.info('Cleaning up failover key after acquiring leader lock...')
self.dcs.manual_failover('', '')
@@ -1839,11 +1839,11 @@ class Ha(object):
def watch(self, timeout: float) -> bool:
# watch on leader key changes if the postgres is running and leader is known and current node is not lock owner
if self._async_executor.busy or not self.cluster or self.cluster.is_unlocked() or self.has_lock(False):
leader_index = None
leader_version = None
else:
leader_index = self.cluster.leader.index if self.cluster.leader else None
leader_version = self.cluster.leader.version if self.cluster.leader else None
return self.dcs.watch(leader_index, timeout)
return self.dcs.watch(leader_version, timeout)
def wakeup(self) -> None:
"""Call of this method will trigger the next run of HA loop if there is
@@ -1868,4 +1868,4 @@ class Ha(object):
data['conn_kwargs'] = conn_kwargs
name = member.name if member else 'remote_member:{}'.format(uuid.uuid1())
return RemoteMember(name, data)
return RemoteMember.from_name_and_data(name, data)
+4 -2
View File
@@ -13,7 +13,7 @@ from patroni.utils import deep_compare
from queue import Queue, Full
from threading import Lock, Thread
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING
_LOGGER = logging.getLogger(__name__)
@@ -249,8 +249,9 @@ class PatroniLogger(Thread):
if not isinstance(self.log_handler, RotatingFileHandler):
new_handler = RotatingFileHandler(os.path.join(config['dir'], __name__))
handler = new_handler or self.log_handler
if TYPE_CHECKING: # pragma: no cover
assert isinstance(handler, RotatingFileHandler)
handler.maxBytes = int(config.get('file_size', 25000000))
handler.maxBytes = int(config.get('file_size', 25000000)) # pyright: ignore [reportGeneralTypeIssues]
handler.backupCount = int(config.get('file_num', 4))
else:
if self.log_handler is None or isinstance(self.log_handler, RotatingFileHandler):
@@ -306,6 +307,7 @@ class PatroniLogger(Thread):
while True:
self._close_old_handlers()
if TYPE_CHECKING: # pragma: no cover
assert self.log_handler is not None
record = self._queue_handler.queue.get(True)
+6 -2
View File
@@ -192,7 +192,7 @@ class Postgresql(object):
"FROM pg_catalog.pg_stat_get_wal_senders() w,"
" pg_catalog.pg_stat_get_activity(w.pid)"
" WHERE w.state = 'streaming') r)").format(self.wal_name, self.lsn_name)
if (not self._global_config or self._global_config.is_synchronous_mode)
if (not self.global_config or self.global_config.is_synchronous_mode)
and self.role in ('master', 'primary', 'promoted') else "'on', '', NULL")
if self._major_version >= 90600:
@@ -375,6 +375,10 @@ class Postgresql(object):
self.config.write_postgresql_conf()
self.reload()
@property
def global_config(self) -> Optional['GlobalConfig']:
return self._global_config
def reset_cluster_info_state(self, cluster: Union[Cluster, None], nofailover: bool = False,
global_config: Optional['GlobalConfig'] = None) -> None:
"""Reset monitoring query cache.
@@ -388,7 +392,7 @@ class Postgresql(object):
:param global_config: last known :class:`GlobalConfig` object
"""
self._cluster_info_state = {}
if cluster and cluster.config and cluster.config.modify_index:
if cluster and cluster.config and cluster.config.modify_version:
self._has_permanent_logical_slots =\
cluster.has_permanent_logical_slots(self.name, nofailover, self.major_version)
+2 -2
View File
@@ -860,9 +860,9 @@ class ConfigHandler(object):
parameters = config['parameters'].copy()
listen_addresses, port = split_host_port(config['listen'], 5432)
parameters.update(cluster_name=self._postgresql.scope, listen_addresses=listen_addresses, port=str(port))
if not self._postgresql._global_config or self._postgresql._global_config.is_synchronous_mode:
if not self._postgresql.global_config or self._postgresql.global_config.is_synchronous_mode:
if self._synchronous_standby_names is None:
if self._postgresql._global_config and self._postgresql._global_config.is_synchronous_mode_strict\
if self._postgresql.global_config and self._postgresql.global_config.is_synchronous_mode_strict\
and self._postgresql.role in ('master', 'primary', 'promoted'):
parameters['synchronous_standby_names'] = '*'
else:
+3 -3
View File
@@ -240,10 +240,10 @@ class SyncHandler(object):
if len(replica_list) > 1 else self._postgresql.last_operation()
if TYPE_CHECKING: # pragma: no cover
assert self._postgresql._global_config is not None
sync_node_count = self._postgresql._global_config.synchronous_node_count\
assert self._postgresql.global_config is not None
sync_node_count = self._postgresql.global_config.synchronous_node_count\
if self._postgresql.supports_multiple_sync else 1
sync_node_maxlag = self._postgresql._global_config.maximum_lag_on_syncnode
sync_node_maxlag = self._postgresql.global_config.maximum_lag_on_syncnode
candidates = CaseInsensitiveSet()
sync_nodes = CaseInsensitiveSet()
+2 -1
View File
@@ -32,7 +32,7 @@ import sys
import time
from enum import IntEnum
from typing import Any, List, NamedTuple, Optional, Tuple
from typing import Any, List, NamedTuple, Optional, Tuple, TYPE_CHECKING
from .. import psycopg
@@ -365,6 +365,7 @@ def main() -> int:
break
time.sleep(RETRY_SLEEP_INTERVAL)
if TYPE_CHECKING: # pragma: no cover
assert exit_code is not None
return exit_code
+2 -1
View File
@@ -11,7 +11,7 @@ import shutil
import socket
import subprocess
from typing import Any, Dict, Union, Iterator, List, Optional as OptionalType
from typing import Any, Dict, Union, Iterator, List, Optional as OptionalType, TYPE_CHECKING
from .utils import parse_int, split_host_port, data_directory_is_empty
from .dcs import dcs_modules
@@ -196,6 +196,7 @@ def get_major_version(bin_dir: OptionalType[str] = None) -> str:
binary = os.path.join(bin_dir, 'postgres')
version = subprocess.check_output([binary, '--version']).decode()
version = re.match(r'^[^\s]+ [^\s]+ (\d+)(\.(\d+))?', version)
if TYPE_CHECKING: # pragma: no cover
assert version is not None
return '.'.join([version.group(1), version.group(3)]) if int(version.group(1)) < 10 else version.group(1)
+2 -2
View File
@@ -34,7 +34,7 @@ def parse_mode(mode: Union[bool, str]) -> str:
def synchronized(func: Callable[..., Any]) -> Callable[..., Any]:
def wrapped(self: 'Watchdog', *args: Any, **kwargs: Any) -> Any:
with self._lock:
with self.lock:
return func(self, *args, **kwargs)
return wrapped
@@ -90,7 +90,7 @@ class Watchdog(object):
def __init__(self, config: Config) -> None:
self.config = WatchdogConfig(config)
self.active_config: WatchdogConfig = self.config
self._lock = RLock()
self.lock = RLock()
self.active = False
if self.config.mode == MODE_OFF:
+1
View File
@@ -1,3 +1,4 @@
# pyright: reportConstantRedefinition=false
import ctypes
import os
import platform
+1 -1
View File
@@ -19,7 +19,7 @@
"reportMissingImports": true,
"reportMissingTypeStubs": false,
"pythonVersion": "3.6",
"pythonVersion": "3.11",
"pythonPlatform": "All",
"typeCheckingMode": "strict"
+2 -2
View File
@@ -1201,7 +1201,7 @@ class TestHa(PostgresInit):
# When we just became primary nobody is sync
self.assertEqual(self.ha.enforce_primary_role('msg', 'promote msg'), 'promote msg')
mock_set_sync.assert_called_once_with(CaseInsensitiveSet())
mock_write_sync.assert_called_once_with('leader', None, index=0)
mock_write_sync.assert_called_once_with('leader', None, version=0)
mock_set_sync.reset_mock()
@@ -1239,7 +1239,7 @@ class TestHa(PostgresInit):
mock_acquire.assert_called_once()
mock_follow.assert_not_called()
mock_promote.assert_called_once()
mock_write_sync.assert_called_once_with('other', None, index=0)
mock_write_sync.assert_called_once_with('other', None, version=0)
def test_disable_sync_when_restarting(self):
self.ha.is_synchronous_mode = true
+2 -1
View File
@@ -339,7 +339,8 @@ class TestPostgresql(BaseTestPostgresql):
@patch.object(Postgresql, 'start', Mock())
def test_follow(self):
self.p.call_nowait(CallbackAction.ON_START)
m = RemoteMember('1', {'restore_command': '2', 'primary_slot_name': 'foo', 'conn_kwargs': {'host': 'bar'}})
m = RemoteMember.from_name_and_data('1', {'restore_command': '2', 'primary_slot_name': 'foo',
'conn_kwargs': {'host': 'bar'}})
self.p.follow(m)
with patch.object(Postgresql, 'ensure_major_version_is_known', Mock(return_value=False)):
self.assertIsNone(self.p.follow(m))