Repository: zalando/patroni
Pull request #3074 (2024-06-09 15:33:40)

drop dependency on ancient mock

https://github.com/testing-cabal/mock

mock is now part of the Python standard library, available as unittest.mock in Python 3.3 onwards.


a-detiste: Hi,

I admit I didn't ran the tests at first attempt.
I'm sending in parralel a huge number of these similar PR and most times Pyflakes will find my mistakes.

>  better fix by doing **import patch from unittest.mock**.

I'm bot sure, different tests have different import sorting style.
A tiny bit confusing, but nothing really bothersome. I tried to keep my diff minimal
and not change the body of the tests.

![image](https://github.com/zalando/patroni/assets/7994192/4c25ca5a-d5dd-45ad-8763-48b22a1afd4c)

Cheers, tests are running

![image](https://github.com/zalando/patroni/assets/7994192/b42d6750-934f-46b7-aca3-31bc61bc871f)


coveralls: ## Pull Request Test Coverage Report for [Build 9455584714](https://coveralls.io/builds/68000992)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **99.862%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/68000992/badge)](https://coveralls.io/builds/68000992) |
| :-- | --: |
| Change from base [Build 9220595698](https://coveralls.io/builds/67684598): |  0.0% |
| Covered Lines: | 13755 |
| Relevant Lines: | 13774 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/.github/workflows/install_deps.py b/.github/workflows/install_deps.py
index d976abbb..700c4b23 100644
--- a/.github/workflows/install_deps.py
+++ b/.github/workflows/install_deps.py
@@ -21,7 +21,7 @@ def install_requirements(what):
         from setup import EXTRAS_REQUIRE, read
     finally:
         sys.path = old_path
-    requirements = ['mock>=2.0.0', 'flake8', 'pytest', 'pytest-cov'] if what == 'all' else ['behave']
+    requirements = ['flake8', 'pytest', 'pytest-cov'] if what == 'all' else ['behave']
     requirements += ['coverage']
     # try to split tests between psycopg2 and psycopg3
     requirements += ['psycopg[binary]'] if sys.version_info >= (3, 8, 0) and\
diff --git a/docs/ENVIRONMENT.rst b/docs/ENVIRONMENT.rst
index 4b45798f..d6c40116 100644
--- a/docs/ENVIRONMENT.rst
+++ b/docs/ENVIRONMENT.rst
@@ -80,7 +80,7 @@ Etcdv3
 Environment names for Etcdv3 are similar as for Etcd, you just need to use ``ETCD3`` instead of ``ETCD`` in the variable name. Example: ``PATRONI_ETCD3_HOST``, ``PATRONI_ETCD3_CACERT``, and so on.
 
 .. warning::
-    Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from Etcd to Etcdv3 just by updating Patroni configuration.
+    Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from Etcd to Etcdv3 just by updating Patroni configuration. In addition, Patroni uses Etcd's gRPC-gateway (proxy) to communicate with the V3 API, which means that TLS common name authentication is not possible.
 
 
 ZooKeeper
diff --git a/docs/yaml_configuration.rst b/docs/yaml_configuration.rst
index 0929404d..adedc00f 100644
--- a/docs/yaml_configuration.rst
+++ b/docs/yaml_configuration.rst
@@ -144,7 +144,7 @@ Etcdv3
 If you want that Patroni works with Etcd cluster via protocol version 3, you need to use the ``etcd3`` section in the Patroni configuration file. All configuration parameters are the same as for ``etcd``.
 
 .. warning::
-    Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from ``etcd`` to ``etcd3`` just by updating Patroni config file.
+    Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from ``etcd`` to ``etcd3`` just by updating Patroni config file. In addition, Patroni uses Etcd's gRPC-gateway (proxy) to communicate with the V3 API, which means that TLS common name authentication is not possible.
 
 
 ZooKeeper
@@ -395,7 +395,7 @@ Tags
 ----
 -  **clonefrom**: ``true`` or ``false``. If set to ``true`` other nodes might prefer to use this node for bootstrap (take ``pg_basebackup`` from). If there are several nodes with ``clonefrom`` tag set to ``true`` the node to bootstrap from will be chosen randomly. The default value is ``false``.
 -  **noloadbalance**: ``true`` or ``false``. If set to ``true`` the node will return HTTP Status Code 503 for the ``GET /replica`` REST API health-check and therefore will be excluded from the load-balancing. Defaults to ``false``.
--  **replicatefrom**: The IP address/hostname of another replica. Used to support cascading replication.
+-  **replicatefrom**: The name of another replica to replicate from. Used to support cascading replication.
 -  **nosync**: ``true`` or ``false``. If set to ``true`` the node will never be selected as a synchronous replica.
 -  **nofailover**: ``true`` or ``false``, controls whether this node is allowed to participate in the leader race and become a leader. Defaults to ``false``, meaning this node _can_ participate in leader races. 
 -  **failover_priority**: integer, controls the priority that this node should have during failover. Nodes with higher priority will be preferred over lower priority nodes if they received/replayed the same amount of WAL. However, nodes with higher values of receive/replay LSN are preferred regardless of their priority. If the ``failover_priority`` is 0 or negative - such node is not allowed to participate in the leader race and to become a leader (similar to ``nofailover: true``).
diff --git a/patroni/dcs/__init__.py b/patroni/dcs/__init__.py
index 971cc3c9..33a39464 100644
--- a/patroni/dcs/__init__.py
+++ b/patroni/dcs/__init__.py
@@ -10,7 +10,7 @@ from copy import deepcopy
 from random import randint
 from threading import Event, Lock
 from typing import Any, Callable, Collection, Dict, Iterator, List, \
-    NamedTuple, Optional, Tuple, Type, TYPE_CHECKING, Union
+    NamedTuple, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
 from urllib.parse import urlparse, urlunparse, parse_qsl
 
 import dateutil.parser
@@ -913,7 +913,9 @@ class Cluster(NamedTuple('Cluster',
 
         :returns: ``True`` if *value* is a physical replication slot, otherwise ``False``.
         """
-        return not value or isinstance(value, dict) and value.get('type', 'physical') == 'physical'
+        return not value \
+            or (isinstance(value, dict) and not Cluster.is_logical_slot(value)
+                and value.get('type', 'physical') == 'physical')
 
     @staticmethod
     def is_logical_slot(value: Union[Any, Dict[str, Any]]) -> bool:
@@ -1179,6 +1181,10 @@ class Cluster(NamedTuple('Cluster',
 
         if global_config.use_slots:
             name = member.name if isinstance(member, Member) else postgresql.name
+
+            if not self.get_slot_name_on_primary(name, member):
+                return False
+
             members = [m for m in self.members if m.replicatefrom == name and m.name != self.leader_name]
             return any(self.should_enforce_hot_standby_feedback(postgresql, m) for m in members)
         return False
@@ -1197,11 +1203,18 @@ class Cluster(NamedTuple('Cluster',
 
         :returns: the slot name on the primary that is in use for physical replication on this node.
         """
-        if tags.nostream:
-            return None
-        replicatefrom = self.get_member(tags.replicatefrom, False) if tags.replicatefrom else None
-        return self.get_slot_name_on_primary(replicatefrom.name, replicatefrom) \
-            if isinstance(replicatefrom, Member) else slot_name_from_member_name(name)
+        seen_nodes: Set[str] = set()
+        while True:
+            seen_nodes.add(name)
+            if tags.nostream:
+                return None
+            replicatefrom = self.get_member(tags.replicatefrom, False) \
+                if tags.replicatefrom and tags.replicatefrom != name else None
+            if not isinstance(replicatefrom, Member):
+                return slot_name_from_member_name(name)
+            if replicatefrom.name in seen_nodes:
+                return None
+            name, tags = replicatefrom.name, replicatefrom
 
     @property
     def timeline(self) -> int:
diff --git a/patroni/postgresql/available_parameters/0_postgres.yml b/patroni/postgresql/available_parameters/0_postgres.yml
index 056f7106..a633b44e 100644
--- a/patroni/postgresql/available_parameters/0_postgres.yml
+++ b/patroni/postgresql/available_parameters/0_postgres.yml
@@ -1,4 +1,7 @@
 parameters:
+  allow_alter_system:
+  - type: Bool
+    version_from: 170000
   allow_in_place_tablespaces:
   - type: Bool
     version_from: 150000
@@ -245,6 +248,12 @@ parameters:
     version_from: 90300
     min_val: 0
     max_val: 1000
+  commit_timestamp_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 0
+    max_val: 131072
+    unit: 8kB
   compute_query_id:
   - type: EnumBool
     version_from: 140000
@@ -299,6 +308,7 @@ parameters:
   db_user_namespace:
   - type: Bool
     version_from: 90300
+    version_till: 170000
   deadlock_timeout:
   - type: Integer
     version_from: 90300
@@ -313,6 +323,12 @@ parameters:
   debug_io_direct:
   - type: String
     version_from: 160000
+  debug_logical_replication_streaming:
+  - type: Enum
+    version_from: 170000
+    possible_values:
+    - buffered
+    - immediate
   debug_parallel_query:
   - type: EnumBool
     version_from: 160000
@@ -406,6 +422,9 @@ parameters:
   enable_gathermerge:
   - type: Bool
     version_from: 100000
+  enable_group_by_reordering:
+  - type: Bool
+    version_from: 170000
   enable_hashagg:
   - type: Bool
     version_from: 90300
@@ -466,6 +485,9 @@ parameters:
   event_source:
   - type: String
     version_from: 90300
+  event_triggers:
+  - type: Bool
+    version_from: 170000
   exit_on_error:
   - type: Bool
     version_from: 90300
@@ -607,6 +629,12 @@ parameters:
   ignore_system_indexes:
   - type: Bool
     version_from: 90300
+  io_combine_limit:
+  - type: Integer
+    version_from: 170000
+    min_val: 1
+    max_val: 32
+    unit: 8kB
   IntervalStyle:
   - type: Enum
     version_from: 90300
@@ -875,6 +903,7 @@ parameters:
   logical_replication_mode:
   - type: Enum
     version_from: 160000
+    version_till: 170000
     possible_values:
     - buffered
     - immediate
@@ -919,6 +948,11 @@ parameters:
     version_from: 100000
     min_val: 0
     max_val: 262143
+  max_notify_queue_pages:
+  - type: Integer
+    version_from: 170000
+    min_val: 64
+    max_val: 2147483647
   max_parallel_apply_workers_per_subscription:
   - type: Integer
     version_from: 160000
@@ -1072,9 +1106,28 @@ parameters:
     min_val: 2
     max_val: 2147483647
     unit: MB
+  multixact_member_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 16
+    max_val: 131072
+    unit: 8kB
+  multixact_offset_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 16
+    max_val: 131072
+    unit: 8kB
+  notify_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 16
+    max_val: 131072
+    unit: 8kB
   old_snapshot_threshold:
   - type: Integer
     version_from: 90600
+    version_till: 170000
     min_val: -1
     max_val: 86400
     unit: min
@@ -1191,6 +1244,12 @@ parameters:
     version_from: 90300
     min_val: 0
     max_val: 1.79769e+308
+  serializable_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 16
+    max_val: 131072
+    unit: 8kB
   session_preload_libraries:
   - type: String
     version_from: 90400
@@ -1283,6 +1342,9 @@ parameters:
   standard_conforming_strings:
   - type: Bool
     version_from: 90300
+  standby_slot_names:
+  - type: String
+    version_from: 170000
   statement_timeout:
   - type: Integer
     version_from: 90300
@@ -1300,6 +1362,15 @@ parameters:
   - type: String
     version_from: 90300
     version_till: 150000
+  subtransaction_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 0
+    max_val: 131072
+    unit: 8kB
+  summarize_wal:
+  - type: Bool
+    version_from: 170000
   superuser_reserved_connections:
   - type: Integer
     version_from: 90300
@@ -1310,6 +1381,9 @@ parameters:
     version_from: 90600
     min_val: 0
     max_val: 262143
+  sync_replication_slots:
+  - type: Bool
+    version_from: 170000
   synchronize_seqscans:
   - type: Bool
     version_from: 90300
@@ -1394,12 +1468,16 @@ parameters:
   timezone_abbreviations:
   - type: String
     version_from: 90300
+  trace_connection_negotiation:
+  - type: Bool
+    version_from: 170000
   trace_notify:
   - type: Bool
     version_from: 90300
   trace_recovery_messages:
   - type: Enum
     version_from: 90300
+    version_till: 170000
     possible_values:
     - debug5
     - debug4
@@ -1452,6 +1530,12 @@ parameters:
   track_wal_io_timing:
   - type: Bool
     version_from: 140000
+  transaction_buffers:
+  - type: Integer
+    version_from: 170000
+    min_val: 0
+    max_val: 131072
+    unit: 8kB
   transaction_deferrable:
   - type: Bool
     version_from: 90300
@@ -1466,6 +1550,12 @@ parameters:
   transaction_read_only:
   - type: Bool
     version_from: 90300
+  transaction_timeout:
+  - type: Integer
+    version_from: 170000
+    min_val: 0
+    max_val: 2147483647
+    unit: ms
   transform_null_equals:
   - type: Bool
     version_from: 90300
@@ -1664,6 +1754,12 @@ parameters:
     min_val: 0
     max_val: 2147483647
     unit: kB
+  wal_summary_keep_time:
+  - type: Integer
+    version_from: 170000
+    min_val: 0
+    max_val: 35791394
+    unit: min
   wal_sync_method:
   - type: Enum
     version_from: 90300
diff --git a/patroni/postgresql/bootstrap.py b/patroni/postgresql/bootstrap.py
index 0f3c4ba5..47dbacfe 100644
--- a/patroni/postgresql/bootstrap.py
+++ b/patroni/postgresql/bootstrap.py
@@ -216,15 +216,13 @@ class Bootstrap(object):
         cmd = config.get('post_bootstrap') or config.get('post_init')
         if cmd:
             r = self._postgresql.connection_pool.conn_kwargs
-            connstring = self._postgresql.config.format_dsn(r, True)
-            if 'host' not in r:
-                # https://www.postgresql.org/docs/current/static/libpq-pgpass.html
-                # A host name of localhost matches both TCP (host name localhost) and Unix domain socket
-                # (pghost empty or the default socket directory) connections coming from the local machine.
-                r['host'] = 'localhost'  # set it to localhost to write into pgpass
 
-            env = self._postgresql.config.write_pgpass(r)
+            # https://www.postgresql.org/docs/current/static/libpq-pgpass.html
+            # A host name of localhost matches both TCP (host name localhost) and Unix domain socket
+            # (pghost empty or the default socket directory) connections coming from the local machine.
+            env = self._postgresql.config.write_pgpass({'host': 'localhost', **r})
             env['PGOPTIONS'] = '-c synchronous_commit=local -c statement_timeout=0'
+            connstring = self._postgresql.config.format_dsn({**r, 'password': None})
 
             try:
                 ret = self._postgresql.cancellable.call(shlex.split(cmd) + [connstring], env=env)
@@ -258,7 +256,7 @@ class Bootstrap(object):
             r = clone_member.conn_kwargs(self._postgresql.config.replication)
             # add the credentials to connect to the replica origin to pgpass.
             env = self._postgresql.config.write_pgpass(r)
-            connstring = self._postgresql.config.format_dsn(r, True)
+            connstring = self._postgresql.config.format_dsn({**r, 'password': None})
         else:
             connstring = ''
             env = os.environ.copy()
diff --git a/patroni/postgresql/config.py b/patroni/postgresql/config.py
index 3755a258..046e6607 100644
--- a/patroni/postgresql/config.py
+++ b/patroni/postgresql/config.py
@@ -114,16 +114,17 @@ def parse_dsn(value: str) -> Optional[Dict[str, str]]:
     """
     Very simple equivalent of `psycopg2.extensions.parse_dsn` introduced in 2.7.0.
     We are not using psycopg2 function in order to remain compatible with 2.5.4+.
-    There is one minor difference though, this function removes `dbname` from the result
-    and sets the `sslmode`, 'gssencmode', and `channel_binding` to `prefer` if it is not present in
-    the connection string. This is necessary to simplify comparison of the old and the new values.
+    There are a few minor differences though, this function sets the `sslmode`, 'gssencmode',
+    and `channel_binding` to `prefer` if they are not present in the connection string.
+    This is necessary to simplify comparison of the old and the new values.
 
     >>> r = parse_dsn('postgresql://u%2Fse:pass@:%2f123,[::1]/db%2Fsdf?application_name=mya%2Fpp&ssl=true')
-    >>> r == {'application_name': 'mya/pp', 'host': ',::1', 'sslmode': 'require',\
+    >>> r == {'application_name': 'mya/pp', 'dbname': 'db/sdf', 'host': ',::1', 'sslmode': 'require',\
               'password': 'pass', 'port': '/123,', 'user': 'u/se', 'gssencmode': 'prefer', 'channel_binding': 'prefer'}
     True
     >>> r = parse_dsn(" host = 'host' dbname = db\\\\ name requiressl=1 ")
-    >>> r == {'host': 'host', 'sslmode': 'require', 'gssencmode': 'prefer', 'channel_binding': 'prefer'}
+    >>> r == {'dbname': 'db name', 'host': 'host', 'sslmode': 'require',\
+              'gssencmode': 'prefer', 'channel_binding': 'prefer'}
     True
     >>> parse_dsn('requiressl = 0\\\\') == {'sslmode': 'prefer', 'gssencmode': 'prefer', 'channel_binding': 'prefer'}
     True
@@ -147,8 +148,6 @@ def parse_dsn(value: str) -> Optional[Dict[str, str]]:
             elif requiressl is not None:
                 ret['sslmode'] = 'prefer'
             ret.setdefault('sslmode', 'prefer')
-        if 'dbname' in ret:
-            del ret['dbname']
         ret.setdefault('gssencmode', 'prefer')
         ret.setdefault('channel_binding', 'prefer')
     return ret
@@ -570,31 +569,30 @@ class ConfigHandler(object):
             ret.setdefault('channel_binding', 'prefer')
         if self._krbsrvname:
             ret['krbsrvname'] = self._krbsrvname
-        if 'dbname' in ret:
-            del ret['dbname']
+        if not ret.get('dbname'):
+            ret['dbname'] = self._postgresql.database
         return ret
 
-    def format_dsn(self, params: Dict[str, Any], include_dbname: bool = False) -> str:
+    def format_dsn(self, params: Dict[str, Any]) -> str:
+        """Format connection string from connection parameters.
+
+        .. note::
+            only parameters from the below list are considered and values are escaped.
+
+        :param params: :class:`dict` object with connection parameters.
+
+        :returns: a connection string in a format "key1=value2 key2=value2"
+        """
         # A list of keywords that can be found in a conninfo string. Follows what is acceptable by libpq
         keywords = ('dbname', 'user', 'passfile' if params.get('passfile') else 'password', 'host', 'port',
                     'sslmode', 'sslcompression', 'sslcert', 'sslkey', 'sslpassword', 'sslrootcert', 'sslcrl',
                     'sslcrldir', 'application_name', 'krbsrvname', 'gssencmode', 'channel_binding',
                     'target_session_attrs')
-        if include_dbname:
-            params = params.copy()
-            if 'dbname' not in params:
-                params['dbname'] = self._postgresql.database
-            # we are abusing information about the necessity of dbname
-            # dsn should contain passfile or password only if there is no dbname in it (it is used in recovery.conf)
-            skip = {'passfile', 'password'}
-        else:
-            skip = {'dbname'}
 
         def escape(value: Any) -> str:
             return re.sub(r'([\'\\ ])', r'\\\1', str(value))
 
-        return ' '.join('{0}={1}'.format(kw, escape(params[kw])) for kw in keywords
-                        if kw not in skip and params.get(kw) is not None)
+        return ' '.join('{0}={1}'.format(kw, escape(params[kw])) for kw in keywords if params.get(kw) is not None)
 
     def _write_recovery_params(self, fd: ConfigWriter, recovery_params: CaseInsensitiveDict) -> None:
         if self._postgresql.major_version >= 90500:
@@ -606,8 +604,7 @@ class ConfigHandler(object):
                 recovery_params.setdefault('pause_at_recovery_target', 'false')
         for name, value in sorted(recovery_params.items()):
             if name == 'primary_conninfo':
-                if 'password' in value and self._postgresql.major_version >= 100000:
-                    self.write_pgpass(value)
+                if self._postgresql.major_version >= 100000 and 'PGPASSFILE' in self.write_pgpass(value):
                     value['passfile'] = self._passfile = self._pgpass
                     self._passfile_mtime = mtime(self._pgpass)
                 value = self.format_dsn(value)
@@ -754,7 +751,7 @@ class ConfigHandler(object):
         if passfile_mtime:
             try:
                 with open(passfile) as f:
-                    wanted_lines = (self._pgpass_line(wanted_primary_conninfo) or '').splitlines()
+                    wanted_lines = (self._pgpass_content(wanted_primary_conninfo) or '').splitlines()
                     file_lines = f.read().splitlines()
                     if set(wanted_lines) == set(file_lines):
                         self._passfile = passfile
@@ -773,12 +770,21 @@ class ConfigHandler(object):
         elif not primary_conninfo:
             return False
 
+        if self._postgresql.major_version < 170000:
+            # we want to compare dbname in primary_conninfo only for v17 onwards
+            wanted_primary_conninfo.pop('dbname', None)
+
         if not self._postgresql.is_starting():
             wal_receiver_primary_conninfo = self._postgresql.primary_conninfo()
             if wal_receiver_primary_conninfo:
                 wal_receiver_primary_conninfo = parse_dsn(wal_receiver_primary_conninfo)
                 # when wal receiver is alive use primary_conninfo from pg_stat_wal_receiver for comparison
                 if wal_receiver_primary_conninfo:
+                    # dbname in pg_stat_wal_receiver is always `replication`, we need to use a "real" one
+                    wal_receiver_primary_conninfo.pop('dbname', None)
+                    dbname = primary_conninfo.get('dbname')
+                    if dbname:
+                        wal_receiver_primary_conninfo['dbname'] = dbname
                     primary_conninfo = wal_receiver_primary_conninfo
                     # There could be no password in the primary_conninfo or it is masked.
                     # Just copy the "desired" value in order to make comparison succeed.
@@ -873,27 +879,38 @@ class ConfigHandler(object):
             os.unlink(name)
 
     @staticmethod
-    def _pgpass_line(record: Dict[str, Any]) -> Optional[str]:
+    def _pgpass_content(record: Dict[str, Any]) -> Optional[str]:
+        """Generate content of `pgpassfile` based on connection parameters.
+
+        .. note::
+            In case if ``host`` is a comma separated string we generate one line per host.
+
+        :param record: :class:`dict` object with connection parameters.
+        :returns: a string with generated content of pgpassfile or ``None`` if there is no ``password``.
+        """
         if 'password' in record:
             def escape(value: Any) -> str:
                 return re.sub(r'([:\\])', r'\\\1', str(value))
 
-            record = {n: escape(record.get(n) or '*') for n in ('host', 'port', 'user', 'password')}
-            # 'host' could be several comma-separated hostnames, in this case
-            # we need to write on pgpass line per host
-            line = ''
-            for hostname in record['host'].split(','):
-                line += hostname + ':{port}:*:{user}:{password}'.format(**record) + '\n'
-            return line.rstrip()
+            # 'host' could be several comma-separated hostnames, in this case we need to write on pgpass line per host
+            hosts = map(escape, filter(None, map(str.strip, (record.get('host') or '*').split(','))))
+            record = {n: escape(record.get(n) or '*') for n in ('port', 'user', 'password')}
+            return '\n'.join('{host}:{port}:*:{user}:{password}'.format(**record, host=host) for host in hosts)
 
     def write_pgpass(self, record: Dict[str, Any]) -> Dict[str, str]:
-        line = self._pgpass_line(record)
-        if not line:
+        """Maybe creates :attr:`_passfile` based on connection parameters.
+
+        :param record: :class:`dict` object with connection parameters.
+
+        :returns: a copy of environment variables, that will include ``PGPASSFILE`` in case if the file was written.
+        """
+        content = self._pgpass_content(record)
+        if not content:
             return os.environ.copy()
 
         with open(self._pgpass, 'w') as f:
             os.chmod(self._pgpass, stat.S_IWRITE | stat.S_IREAD)
-            f.write(line)
+            f.write(content)
 
         return {**os.environ, 'PGPASSFILE': self._pgpass}
 
diff --git a/patroni/postgresql/postmaster.py b/patroni/postgresql/postmaster.py
index 97eb10e4..359067c2 100644
--- a/patroni/postgresql/postmaster.py
+++ b/patroni/postgresql/postmaster.py
@@ -176,11 +176,15 @@ class PostmasterProcess(psutil.Process):
             return not self.is_running()
 
     def wait_for_user_backends_to_close(self, stop_timeout: Optional[float]) -> None:
-        # These regexps are cross checked against versions PostgreSQL 9.1 .. 16
+        # These regexps are cross checked against versions PostgreSQL 9.1 .. 17
         aux_proc_re = re.compile("(?:postgres:)( .*:)? (?:(?:archiver|startup|autovacuum launcher|autovacuum worker|"
                                  "checkpointer|logger|stats collector|wal receiver|wal writer|writer)(?: process  )?|"
                                  "walreceiver|wal sender process|walsender|walwriter|background writer|"
-                                 "logical replication launcher|logical replication worker for|bgworker:) ")
+                                 "logical replication launcher|logical replication worker for subscription|"
+                                 "logical replication tablesync worker for subscription|"
+                                 "logical replication parallel apply worker for subscription|"
+                                 "logical replication apply worker for subscription|"
+                                 "slotsync worker|walsummarizer|bgworker:) ")
 
         try:
             children = self.children()
diff --git a/patroni/postgresql/rewind.py b/patroni/postgresql/rewind.py
index c3461630..91cddd59 100644
--- a/patroni/postgresql/rewind.py
+++ b/patroni/postgresql/rewind.py
@@ -412,11 +412,22 @@ class Rewind(object):
             except Exception as e:
                 logger.warning('Unable to clean %s: %r', replslot_dir, e)
 
-    def pg_rewind(self, r: Dict[str, Any]) -> bool:
-        # prepare pg_rewind connection
-        env = self._postgresql.config.write_pgpass(r)
+    def pg_rewind(self, conn_kwargs: Dict[str, Any]) -> bool:
+        """Do pg_rewind.
+
+        .. note::
+            If ``pg_rewind`` doesn't support ``--restore-target-wal`` parameter and exited with non zero code,
+            Patroni will parse stderr/stdout to figure out if it failed due to a missing WAL file and will
+            repeat an attempt after downloading the missing file using ``restore_command``.
+
+        :param conn_kwargs: :class:`dict` object with connection parameters.
+
+        :returns: ``True`` if ``pg_rewind`` finished successfully, ``False`` otherwise.
+        """
+        # prepare pg_rewind connection string
+        env = self._postgresql.config.write_pgpass(conn_kwargs)
         env.update(LANG='C', LC_ALL='C', PGOPTIONS='-c statement_timeout=0')
-        dsn = self._postgresql.config.format_dsn(r, True)
+        dsn = self._postgresql.config.format_dsn({**conn_kwargs, 'password': None})
         logger.info('running pg_rewind from %s', dsn)
 
         restore_command = (self._postgresql.config.get('recovery_conf') or EMPTY_DICT).get('restore_command') \
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 36fd575c..0393bfb7 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -2,7 +2,6 @@ psycopg2-binary
 behave
 coverage
 flake8>=3.0.0
-mock
 pytest-cov
 pytest
 setuptools
diff --git a/tox.ini b/tox.ini
index 1731548a..339344a5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -69,7 +69,6 @@ commands_post =
     - {tty:{env:OPEN_CMD} "{toxworkdir}{/}pytest_report_{env_name}.html":true}
 deps =
     -r requirements.txt
-    mock>=2.0.0
     pytest
     pytest-cov
     pytest-html


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index aa15de8b..24b18226 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -2,8 +2,7 @@ import datetime
 import os
 import shutil
 import unittest
-
-from mock import Mock, PropertyMock, patch
+from unittest.mock import Mock, PropertyMock, patch
 
 import urllib3
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 678d896c..bc7f5263 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -5,8 +5,8 @@ import socket
 
 from http.server import HTTPServer
 from io import BytesIO as IO
-from mock import Mock, PropertyMock, patch
 from socketserver import ThreadingMixIn
+from unittest.mock import Mock, PropertyMock, patch
 
 from patroni import global_config
 from patroni.api import RestApiHandler, RestApiServer
diff --git a/tests/test_async_executor.py b/tests/test_async_executor.py
index e5dea105..08a91917 100644
--- a/tests/test_async_executor.py
+++ b/tests/test_async_executor.py
@@ -1,6 +1,6 @@
 import unittest
+from unittest.mock import Mock, patch
 
-from mock import Mock, patch
 from patroni.async_executor import AsyncExecutor, CriticalTask
 from threading import Thread
 
diff --git a/tests/test_aws.py b/tests/test_aws.py
index c291dbd2..69ca3285 100644
--- a/tests/test_aws.py
+++ b/tests/test_aws.py
@@ -3,7 +3,7 @@ import botocore.awsrequest
 import sys
 import unittest
 
-from mock import Mock, PropertyMock, patch
+from unittest.mock import Mock, PropertyMock, patch
 from collections import namedtuple
 from patroni.scripts.aws import AWSConnection, main as _main
 
diff --git a/tests/test_barman.py b/tests/test_barman.py
index 254ff134..eda9cdda 100644
--- a/tests/test_barman.py
+++ b/tests/test_barman.py
@@ -1,7 +1,7 @@
 import logging
-import mock
-from mock import MagicMock, Mock, patch
 import unittest
+from unittest import mock
+from unittest.mock import MagicMock, Mock, patch
 from urllib3.exceptions import MaxRetryError
 
 from patroni.scripts.barman.cli import main
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index eaedc705..37725ae5 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -1,7 +1,6 @@
 import os
 import sys
-
-from mock import Mock, PropertyMock, patch
+from unittest.mock import Mock, PropertyMock, patch
 
 from patroni.async_executor import CriticalTask
 from patroni.collections import CaseInsensitiveDict
diff --git a/tests/test_callback_executor.py b/tests/test_callback_executor.py
index 51c915d9..6ec53a3d 100644
--- a/tests/test_callback_executor.py
+++ b/tests/test_callback_executor.py
@@ -1,7 +1,7 @@
 import psutil
 import unittest
+from unittest.mock import Mock, patch
 
-from mock import Mock, patch
 from patroni.postgresql.callback_executor import CallbackExecutor
 
 
diff --git a/tests/test_cancellable.py b/tests/test_cancellable.py
index cd078fa6..23510cf9 100644
--- a/tests/test_cancellable.py
+++ b/tests/test_cancellable.py
@@ -1,7 +1,7 @@
 import psutil
 import unittest
+from unittest.mock import Mock, patch
 
-from mock import Mock, patch
 from patroni.exceptions import PostgresException
 from patroni.postgresql.cancellable import CancellableSubprocess
 
diff --git a/tests/test_citus.py b/tests/test_citus.py
index 7cf26e2c..bbda7672 100644
--- a/tests/test_citus.py
+++ b/tests/test_citus.py
@@ -1,5 +1,6 @@
 import time
-from mock import Mock, patch, PropertyMock
+from unittest.mock import Mock, patch, PropertyMock
+
 from patroni.postgresql.mpp.citus import CitusHandler
 from patroni.psycopg import ProgrammingError
 
diff --git a/tests/test_config.py b/tests/test_config.py
index 2ca38cba..5b10dfc0 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,7 +4,7 @@ import unittest
 import io
 
 from copy import deepcopy
-from mock import MagicMock, Mock, patch
+from unittest.mock import MagicMock, Mock, patch
 
 from patroni import global_config
 from patroni.config import ClusterConfig, Config, ConfigParseError
diff --git a/tests/test_config_generator.py b/tests/test_config_generator.py
index 640c92b6..d6311a03 100644
--- a/tests/test_config_generator.py
+++ b/tests/test_config_generator.py
@@ -5,7 +5,7 @@ import yaml
 
 from . import MockConnect, MockCursor, MockConnectionInfo
 from copy import deepcopy
-from mock import MagicMock, Mock, PropertyMock, mock_open as _mock_open, patch
+from unittest.mock import MagicMock, Mock, PropertyMock, mock_open as _mock_open, patch
 
 from patroni.__main__ import main as _main
 from patroni.config import Config
diff --git a/tests/test_consul.py b/tests/test_consul.py
index 8c7eae1d..d18913e0 100644
--- a/tests/test_consul.py
+++ b/tests/test_consul.py
@@ -1,8 +1,8 @@
 import consul
 import unittest
+from unittest.mock import Mock, PropertyMock, patch
 
 from consul import ConsulException, NotFound
-from mock import Mock, PropertyMock, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.consul import AbstractDCS, Cluster, Consul, ConsulInternalError, \
     ConsulError, ConsulClient, HTTPClient, InvalidSessionTTL, InvalidSession, RetryFailedError
diff --git a/tests/test_ctl.py b/tests/test_ctl.py
index 42d78214..5b56c93a 100644
--- a/tests/test_ctl.py
+++ b/tests/test_ctl.py
@@ -1,12 +1,10 @@
 import click
 import etcd
-import mock
 import os
 import unittest
 
 from click.testing import CliRunner
 from datetime import datetime, timedelta
-from mock import patch, Mock, PropertyMock
 from patroni import global_config
 from patroni.ctl import ctl, load_config, output_members, get_dcs, parse_dcs, \
     get_all_members, get_any_member, get_cursor, query_member, PatroniCtlException, apply_config_changes, \
@@ -17,6 +15,8 @@ from patroni.postgresql.mpp import get_mpp
 from patroni.psycopg import OperationalError
 from patroni.utils import tzutc
 from prettytable import PrettyTable, ALL
+from unittest import mock
+from unittest.mock import patch, Mock, PropertyMock
 from urllib3 import PoolManager
 
 from . import MockConnect, MockCursor, MockResponse, psycopg_connect
diff --git a/tests/test_etcd.py b/tests/test_etcd.py
index d7a423be..4c56c425 100644
--- a/tests/test_etcd.py
+++ b/tests/test_etcd.py
@@ -4,12 +4,12 @@ import socket
 import unittest
 
 from dns.exception import DNSException
-from mock import Mock, PropertyMock, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.etcd import AbstractDCS, EtcdClient, Cluster, Etcd, EtcdError, DnsCachingResolver
 from patroni.exceptions import DCSError
 from patroni.postgresql.mpp import get_mpp
 from patroni.utils import Retry
+from unittest.mock import Mock, PropertyMock, patch
 from urllib3.exceptions import ReadTimeoutError
 
 from . import SleepException, MockResponse, requests_get
diff --git a/tests/test_etcd3.py b/tests/test_etcd3.py
index ffcabd50..bf52af07 100644
--- a/tests/test_etcd3.py
+++ b/tests/test_etcd3.py
@@ -2,8 +2,8 @@ import etcd
 import json
 import unittest
 import urllib3
+from unittest.mock import Mock, PropertyMock, patch
 
-from mock import Mock, PropertyMock, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.etcd import DnsCachingResolver
 from patroni.dcs.etcd3 import PatroniEtcd3Client, Cluster, Etcd3, Etcd3Client, \
diff --git a/tests/test_exhibitor.py b/tests/test_exhibitor.py
index 5a72eb21..0620292c 100644
--- a/tests/test_exhibitor.py
+++ b/tests/test_exhibitor.py
@@ -1,7 +1,7 @@
 import unittest
 import urllib3
+from unittest.mock import Mock, patch
 
-from mock import Mock, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.exhibitor import ExhibitorEnsembleProvider, Exhibitor
 from patroni.dcs.zookeeper import ZooKeeperError
diff --git a/tests/test_file_perm.py b/tests/test_file_perm.py
index e85b3c65..09af602b 100644
--- a/tests/test_file_perm.py
+++ b/tests/test_file_perm.py
@@ -1,7 +1,6 @@
 import unittest
 import stat
-
-from mock import Mock, patch
+from unittest.mock import Mock, patch
 
 from patroni.file_perm import pg_perm
 
diff --git a/tests/test_ha.py b/tests/test_ha.py
index d6f70124..5f6069d6 100644
--- a/tests/test_ha.py
+++ b/tests/test_ha.py
@@ -2,8 +2,8 @@ import datetime
 import etcd
 import os
 import sys
+from unittest.mock import Mock, MagicMock, PropertyMock, patch, mock_open
 
-from mock import Mock, MagicMock, PropertyMock, patch, mock_open
 from patroni import global_config
 from patroni.collections import CaseInsensitiveSet
 from patroni.config import Config
diff --git a/tests/test_kubernetes.py b/tests/test_kubernetes.py
index 59887e30..4bb2da19 100644
--- a/tests/test_kubernetes.py
+++ b/tests/test_kubernetes.py
@@ -1,13 +1,13 @@
 import base64
 import datetime
 import json
-import mock
 import socket
 import time
 import unittest
 import urllib3
+from unittest import mock
+from unittest.mock import Mock, PropertyMock, mock_open, patch
 
-from mock import Mock, PropertyMock, mock_open, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.kubernetes import Cluster, k8s_client, k8s_config, K8sConfig, K8sConnectionFailed, \
     K8sException, K8sObject, Kubernetes, KubernetesError, KubernetesRetriableException, \
diff --git a/tests/test_log.py b/tests/test_log.py
index 48020ff4..90f3deff 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -4,11 +4,11 @@ import sys
 import unittest
 import yaml
 from io import StringIO
+from queue import Queue, Full
+from unittest.mock import Mock, patch
 
-from mock import Mock, patch
 from patroni.config import Config
 from patroni.log import PatroniLogger
-from queue import Queue, Full
 
 try:
     from pythonjsonlogger import jsonlogger
diff --git a/tests/test_patroni.py b/tests/test_patroni.py
index da65ebaa..fb5c0fc7 100644
--- a/tests/test_patroni.py
+++ b/tests/test_patroni.py
@@ -4,10 +4,10 @@ import os
 import signal
 import time
 import unittest
+from unittest.mock import Mock, PropertyMock, patch
 
 import patroni.config as config
 from http.server import HTTPServer
-from mock import Mock, PropertyMock, patch
 from patroni.api import RestApiServer
 from patroni.async_executor import AsyncExecutor
 from patroni.dcs import Cluster, Member
diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py
index 264adac1..ff76ade1 100644
--- a/tests/test_postgresql.py
+++ b/tests/test_postgresql.py
@@ -6,8 +6,8 @@ import subprocess
 import time
 
 from copy import deepcopy
-from mock import Mock, MagicMock, PropertyMock, patch, mock_open
 from pathlib import Path
+from unittest.mock import Mock, MagicMock, PropertyMock, patch, mock_open
 
 import patroni.psycopg as psycopg
 
@@ -294,7 +294,7 @@ class TestPostgresql(BaseTestPostgresql):
             mock_get_pg_settings.return_value['recovery_min_apply_delay'][1] = '1'
             self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
             mock_get_pg_settings.return_value['primary_conninfo'][1] = 'host=1 target_session_attrs=read-write'\
-                + ' passfile=' + re.sub(r'([\'\\ ])', r'\\\1', self.p.config._pgpass)
+                + ' dbname=postgres passfile=' + re.sub(r'([\'\\ ])', r'\\\1', self.p.config._pgpass)
             mock_get_pg_settings.return_value['recovery_min_apply_delay'][1] = '0'
             self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
             self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': conninfo.copy()})
diff --git a/tests/test_postmaster.py b/tests/test_postmaster.py
index 69e790d2..0924e6f7 100644
--- a/tests/test_postmaster.py
+++ b/tests/test_postmaster.py
@@ -1,12 +1,12 @@
 import multiprocessing
 import psutil
 import unittest
+from unittest.mock import Mock, patch, mock_open
 
-from mock import Mock, patch, mock_open
 from patroni.postgresql.postmaster import PostmasterProcess
 
 
-class MockProcess(object):
+class MockProcess:
     def __init__(self, target, args):
         self.target = target
         self.args = args
@@ -132,14 +132,16 @@ class TestPostmasterProcess(unittest.TestCase):
         c2.cmdline = Mock(return_value=["postgres: postgres postgres [local] idle"])
         c3 = Mock()
         c3.cmdline = Mock(side_effect=psutil.NoSuchProcess(123))
+        c4 = Mock()
+        c4.cmdline = Mock(return_value=['postgres: slotsync worker '])
         mock_wait.return_value = ([], [c2])
-        with patch('psutil.Process.children', Mock(return_value=[c1, c2, c3])):
+        with patch('psutil.Process.children', Mock(return_value=[c1, c2, c3, c4])):
             proc = PostmasterProcess(123)
             self.assertIsNone(proc.wait_for_user_backends_to_close(1))
             mock_wait.assert_called_with([c2], 1)
 
         mock_wait.return_value = ([c2], [])
-        with patch('psutil.Process.children', Mock(return_value=[c1, c2, c3])):
+        with patch('psutil.Process.children', Mock(return_value=[c1, c2, c3, c4])):
             proc = PostmasterProcess(123)
             proc.wait_for_user_backends_to_close(1)
 
diff --git a/tests/test_raft.py b/tests/test_raft.py
index b25196f6..b1a778c3 100644
--- a/tests/test_raft.py
+++ b/tests/test_raft.py
@@ -2,8 +2,8 @@ import os
 import unittest
 import tempfile
 import time
+from unittest.mock import Mock, PropertyMock, patch
 
-from mock import Mock, PropertyMock, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.raft import Cluster, DynMemberSyncObj, KVStoreTTL, \
     Raft, RaftError, SyncObjUtility, TCPTransport, _TCPTransport
diff --git a/tests/test_raft_controller.py b/tests/test_raft_controller.py
index 347517a1..fdd09f71 100644
--- a/tests/test_raft_controller.py
+++ b/tests/test_raft_controller.py
@@ -1,8 +1,8 @@
 import logging
 import os
 import unittest
+from unittest.mock import Mock, patch
 
-from mock import Mock, patch
 from pysyncobj import SyncObj
 from patroni.config import Config
 from patroni.raft_controller import RaftController, main as _main
diff --git a/tests/test_rewind.py b/tests/test_rewind.py
index e7ef224f..a51a8bec 100644
--- a/tests/test_rewind.py
+++ b/tests/test_rewind.py
@@ -1,4 +1,4 @@
-from mock import Mock, PropertyMock, patch, mock_open
+from unittest.mock import Mock, PropertyMock, patch, mock_open
 
 from patroni.postgresql import Postgresql
 from patroni.postgresql.cancellable import CancellableSubprocess
@@ -103,7 +103,10 @@ class TestRewind(BaseTestPostgresql):
     @patch.object(Postgresql, 'stop', Mock(return_value=False))
     @patch.object(Postgresql, 'start', Mock())
     def test_execute(self, mock_checkpoint):
-        self.r.execute(self.leader)
+        with patch('patroni.postgresql.rewind.logger.info') as mock_logger:
+            self.r.execute(self.leader)
+            self.assertEqual(mock_logger.call_args_list[0][0],
+                             ('running pg_rewind from %s', 'dbname=postgres user=foo host=127.0.0.1 port=5435'))
         with patch.object(Postgresql, 'major_version', PropertyMock(return_value=130000)):
             self.r.execute(self.leader)
             with patch.object(MockCursor, 'fetchone', Mock(side_effect=Exception)):
diff --git a/tests/test_slots.py b/tests/test_slots.py
index a7112f81..2675c9c7 100644
--- a/tests/test_slots.py
+++ b/tests/test_slots.py
@@ -1,10 +1,8 @@
-import mock
 import os
 import unittest
-
-
-from mock import Mock, PropertyMock, patch
 from threading import Thread
+from unittest import mock
+from unittest.mock import Mock, PropertyMock, patch
 
 from patroni import global_config, psycopg
 from patroni.dcs import Cluster, ClusterConfig, Member, Status, SyncState
@@ -186,6 +184,32 @@ class TestSlotsHandler(BaseTestPostgresql):
             cluster.get_slot_name_on_primary(self.p.name, stream_node),
             'test_4')
 
+    def test_get_slot_name_on_primary(self):
+        node1 = Member(0, 'node1', 28, {
+            'state': 'running', 'conn_url': 'postgres://replicator:rep-pass@127.0.0.1:5436/postgres',
+            'tags': {'replicatefrom': 'node2'}
+        })
+        node2 = Member(0, 'node2', 28, {
+            'state': 'running', 'conn_url': 'postgres://replicator:rep-pass@127.0.0.1:5436/postgres',
+            'tags': {'replicatefrom': 'node1'}
+        })
+        cluster = Cluster(True, None, self.leader, Status.empty(), [self.leadermem, node1, node2],
+                          None, SyncState.empty(), None, None)
+        self.assertIsNone(cluster.get_slot_name_on_primary('node1', node1))
+
+    def test_should_enforce_hot_standby_feedback(self):
+        node1 = Member(0, 'postgresql0', 28, {
+            'state': 'running', 'conn_url': 'postgres://replicator:rep-pass@127.0.0.1:5436/postgres',
+            'tags': {'replicatefrom': 'postgresql1'}
+        })
+        node2 = Member(0, 'postgresql1', 28, {
+            'state': 'running', 'conn_url': 'postgres://replicator:rep-pass@127.0.0.1:5436/postgres',
+            'tags': {'replicatefrom': 'postgresql0'}
+        })
+        cluster = Cluster(True, None, self.leader, Status.empty(), [self.leadermem, node1, node2],
+                          None, SyncState.empty(), None, None)
+        self.assertFalse(cluster.should_enforce_hot_standby_feedback(self.p, node1))
+
     @patch.object(Postgresql, 'is_primary', Mock(return_value=False))
     def test__ensure_logical_slots_replica(self):
         self.p.set_role('replica')
diff --git a/tests/test_sync.py b/tests/test_sync.py
index fbfccf4c..50235192 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -1,6 +1,5 @@
 import os
-
-from mock import Mock, patch, PropertyMock
+from unittest.mock import Mock, patch, PropertyMock
 
 from patroni import global_config
 from patroni.collections import CaseInsensitiveSet
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 023c8823..de20927a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,5 @@
 import unittest
-
-from mock import Mock, patch
+from unittest.mock import Mock, patch
 
 from patroni.exceptions import PatroniException
 from patroni.utils import Retry, RetryFailedError, enable_keepalive, polling_loop, validate_directory, unquote
diff --git a/tests/test_validator.py b/tests/test_validator.py
index 55f1ef7f..098c7b07 100644
--- a/tests/test_validator.py
+++ b/tests/test_validator.py
@@ -5,7 +5,7 @@ import tempfile
 import unittest
 
 from io import StringIO
-from mock import Mock, patch, mock_open
+from unittest.mock import Mock, patch, mock_open
 from patroni.dcs import dcs_modules
 from patroni.validator import schema, Directory, Schema
 
diff --git a/tests/test_wale_restore.py b/tests/test_wale_restore.py
index 0a1150ab..a3a4274c 100644
--- a/tests/test_wale_restore.py
+++ b/tests/test_wale_restore.py
@@ -1,12 +1,11 @@
 import subprocess
 import unittest
+from threading import current_thread
+from unittest.mock import Mock, PropertyMock, patch, mock_open
 
 import patroni.psycopg as psycopg
-
-from mock import Mock, PropertyMock, patch, mock_open
 from patroni.scripts import wale_restore
 from patroni.scripts.wale_restore import WALERestore, main as _main, get_major_version
-from threading import current_thread
 
 from . import MockConnect, psycopg_connect
 
diff --git a/tests/test_watchdog.py b/tests/test_watchdog.py
index af37a5ff..eb70778b 100644
--- a/tests/test_watchdog.py
+++ b/tests/test_watchdog.py
@@ -3,14 +3,14 @@ import patroni.watchdog.linux as linuxwd
 import sys
 import unittest
 import os
+from unittest.mock import patch, Mock, PropertyMock
 
-from mock import patch, Mock, PropertyMock
 from patroni.watchdog import Watchdog, WatchdogError
 from patroni.watchdog.base import NullWatchdog
 from patroni.watchdog.linux import LinuxWatchdogDevice
 
 
-class MockDevice(object):
+class MockDevice:
     def __init__(self, fd, filename, flag):
         self.fd = fd
         self.filename = filename
diff --git a/tests/test_zookeeper.py b/tests/test_zookeeper.py
index 1bb6a23b..4aa46194 100644
--- a/tests/test_zookeeper.py
+++ b/tests/test_zookeeper.py
@@ -1,12 +1,12 @@
 import select
 import unittest
+from unittest.mock import Mock, PropertyMock, patch
 
 from kazoo.client import KazooClient
 from kazoo.exceptions import NoNodeError, NodeExistsError
 from kazoo.handlers.threading import SequentialThreadingHandler
 from kazoo.protocol.states import KeeperState, WatchedEvent, ZnodeStat
 from kazoo.retry import RetryFailedError
-from mock import Mock, PropertyMock, patch
 from patroni.dcs import get_dcs
 from patroni.dcs.zookeeper import Cluster, PatroniKazooClient, \
     PatroniSequentialThreadingHandler, ZooKeeper, ZooKeeperError