Repository: zalando/patroni
Pull request #3007 (2024-01-16 15:06:58)

Release v3.2.2
- update release notes
- bump Patroni version
- bump pyright version and fix reported issues
- improve compatibility with legacy psycopg2

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


* **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.808%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65098685/badge)](https://coveralls.io/builds/65098685) |
| :-- | --: |
| Change from base [Build 7539724004](https://coveralls.io/builds/65091330): |  0.0% |
| Covered Lines: | 13543 |
| Relevant Lines: | 13569 |

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

barthisrael: 👍 
barthisrael: Just a side comment: it seems we are missing to cherry-pick the `pending_restart` stuff into `REL_3_2`.
CyberDem0n: It is cherry-picked, I just forgot to push `REL_3_2`.
hughcapet: 👍 

Patch:
diff --git a/docs/releases.rst b/docs/releases.rst
index c68977cd..ac167bd0 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -3,6 +3,56 @@
 Release notes
 =============
 
+Version 3.2.2
+-------------
+
+**Bugfixes**
+
+- Don't let replica restore initialize key when DCS was wiped (Alexander Kukushkin)
+
+  It was happening in the method where Patroni was supposed to take over a standalone PG cluster.
+
+- Use consistent read when fetching just updated sync key from Consul (Alexander Kukushkin)
+
+  Consul doesn't provide any interface to immediately get ``ModifyIndex`` for the key that we just updated, therefore we have to perform an explicit read operation. Since stale reads are allowed by default, we sometimes used to get an outdated version of the key.
+
+- Reload Postgres config if a parameter that requires restart was reset to the original value (Polina Bungina)
+
+  Previously Patroni wasn't updating the config, but only resetting the ``pending_restart``.
+
+- Fix erroneous inverted logic of the confirmation prompt message when doing a failover to an async candidate in synchronous mode (Polina Bungina)
+
+  The problem existed only in ``patronictl``.
+
+- Exclude leader from failover candidates in ``patronictl`` (Polina Bungina)
+
+  If the cluster is healthy, failing over to an existing leader is no-op.
+
+- Create Citus database and extension idempotently (Alexander Kukushkin, Zhao Junwang)
+
+  It will allow to create them in the ``post_bootstrap`` script in case if there is a need to add some more dependencies to the Citus database.
+
+- Don't filter our contradictory ``nofailover`` tag (Polina Bungina)
+
+  The configuration ``{nofailover: false, failover_priority: 0}`` set on a node didn't allow it to participate in the race, while it should, because ``nofailover`` tag should take precedence.
+
+- Fixed PyInstaller frozen issue (Sophia Ruan)
+
+  The ``freeze_support()`` was called after ``argparse`` and as a result, Patroni wasn't able to start Postgres.
+
+- Fixed bug in the config generator for ``patronictl`` and ``Citus`` configuration (Israel Barth Rubio)
+
+  It prevented ``patronictl`` and ``Citus`` configuration parameters set via environment variables from being written into the generated config.
+
+- Restore recovery GUCs and some Patroni-managed parameters when joining a running standby (Alexander Kukushkin)
+
+  Patroni was failing to restart Postgres v12 onwards with an error about missing ``port`` in one of the internal structures.
+
+- Fixes around ``pending_restart`` flag (Polina Bungina)
+
+  Don't expose ``pending_restart`` when in custom bootstrap with ``recovery_target_action = promote`` or when someone changed ``hot_standby`` or ``wal_log_hints`` using for example ``ALTER SYSTEM``.
+
+
 Version 3.2.1
 -------------
 
diff --git a/patroni/ctl.py b/patroni/ctl.py
index 6a5fe0e6..4e7756be 100644
--- a/patroni/ctl.py
+++ b/patroni/ctl.py
@@ -1410,7 +1410,7 @@ def switchover(cluster_name: str, group: Optional[int], leader: Optional[str],
 
 
 def generate_topology(level: int, member: Dict[str, Any],
-                      topology: Dict[str, List[Dict[str, Any]]]) -> Iterator[Dict[str, Any]]:
+                      topology: Dict[Optional[str], List[Dict[str, Any]]]) -> Iterator[Dict[str, Any]]:
     """Recursively yield members with their names adjusted according to their *level* in the cluster topology.
 
     .. note::
@@ -1473,7 +1473,7 @@ def topology_sort(members: List[Dict[str, Any]]) -> Iterator[Dict[str, Any]]:
     :yields: *members* sorted by level in the topology, and with a new ``name`` value according to their level
         in the topology.
     """
-    topology: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
+    topology: Dict[Optional[str], List[Dict[str, Any]]] = defaultdict(list)
     leader = next((m for m in members if m['role'].endswith('leader')), {'name': None})
     replicas = set(member['name'] for member in members if not member['role'].endswith('leader'))
     for member in members:
diff --git a/patroni/postgresql/mpp/citus.py b/patroni/postgresql/mpp/citus.py
index f3d6394c..295537df 100644
--- a/patroni/postgresql/mpp/citus.py
+++ b/patroni/postgresql/mpp/citus.py
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING
 
 from . import AbstractMPP, AbstractMPPHandler
 from ...dcs import Cluster
-from ...psycopg import connect, quote_ident, DuplicateDatabase
+from ...psycopg import connect, quote_ident, ProgrammingError
 from ...utils import parse_int
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -392,8 +392,11 @@ class CitusHandler(Citus, AbstractMPPHandler, Thread):
                 with conn.cursor() as cur:
                     cur.execute('CREATE DATABASE {0}'.format(
                         quote_ident(self._config['database'], conn)).encode('utf-8'))
-            except DuplicateDatabase as e:
-                logger.debug('Exception when creating database: %r', e)
+            except ProgrammingError as exc:
+                if exc.diag.sqlstate == '42P04':  # DuplicateDatabase
+                    logger.debug('Exception when creating database: %r', exc)
+                else:
+                    raise exc
             finally:
                 conn.close()
 
diff --git a/patroni/psycopg.py b/patroni/psycopg.py
index 5d47ad5c..4a92047c 100644
--- a/patroni/psycopg.py
+++ b/patroni/psycopg.py
@@ -9,8 +9,7 @@ if TYPE_CHECKING:  # pragma: no cover
     from psycopg import Connection
     from psycopg2 import connection, cursor
 
-__all__ = ['connect', 'quote_ident', 'quote_literal', 'DatabaseError', 'Error', 'OperationalError', 'ProgrammingError',
-           'DuplicateDatabase']
+__all__ = ['connect', 'quote_ident', 'quote_literal', 'DatabaseError', 'Error', 'OperationalError', 'ProgrammingError']
 
 _legacy = False
 try:
@@ -19,7 +18,6 @@ try:
     if parse_version(__version__) < MIN_PSYCOPG2:
         raise ImportError
     from psycopg2 import connect as _connect, Error, DatabaseError, OperationalError, ProgrammingError
-    from psycopg2.errors import DuplicateDatabase
     from psycopg2.extensions import adapt
 
     try:
@@ -45,7 +43,6 @@ try:
         return value.getquoted().decode('utf-8')
 except ImportError:
     from psycopg import connect as __connect, sql, Error, DatabaseError, OperationalError, ProgrammingError
-    from psycopg.errors import DuplicateDatabase
 
     def _connect(dsn: Optional[str] = None, **kwargs: Any) -> 'Connection[Any]':
         """Call :func:`psycopg.connect` with *dsn* and ``**kwargs``.
diff --git a/patroni/version.py b/patroni/version.py
index 96592c7d..232dc4a9 100644
--- a/patroni/version.py
+++ b/patroni/version.py
@@ -2,4 +2,4 @@
 
 :var __version__: the current Patroni version.
 """
-__version__ = '3.2.1'
+__version__ = '3.2.2'


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f1f55e2a..098bd010 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -174,7 +174,7 @@ jobs:
 
     - uses: jakebailey/pyright-action@v1
       with:
-        version: 1.1.338
+        version: 1.1.347
 
   docs:
     runs-on: ubuntu-latest
diff --git a/tests/__init__.py b/tests/__init__.py
index d6f0fb6f..82bd7b61 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -129,8 +129,6 @@ class MockCursor(object):
             sql = sql.decode('utf-8')
         if sql.startswith('blabla'):
             raise psycopg.ProgrammingError()
-        if sql.startswith('CREATE DATABASE'):
-            raise psycopg.DuplicateDatabase()
         elif sql == 'CHECKPOINT' or sql.startswith('SELECT pg_catalog.pg_create_'):
             raise psycopg.OperationalError()
         elif sql.startswith('RetryFailedError'):
diff --git a/tests/test_citus.py b/tests/test_citus.py
index f1d8a020..3d78668f 100644
--- a/tests/test_citus.py
+++ b/tests/test_citus.py
@@ -1,6 +1,7 @@
 import time
-from mock import Mock, patch
+from mock import Mock, patch, PropertyMock
 from patroni.postgresql.mpp.citus import CitusHandler
+from patroni.psycopg import ProgrammingError
 
 from . import BaseTestPostgresql, MockCursor, psycopg_connect, SleepException
 from .test_ha import get_cluster_initialized_with_leader
@@ -162,6 +163,11 @@ class TestCitus(BaseTestPostgresql):
     @patch('patroni.postgresql.mpp.citus.connect', psycopg_connect)
     @patch('patroni.postgresql.mpp.citus.quote_ident', Mock())
     def test_bootstrap_duplicate_database(self, mock_logger):
-        self.c.bootstrap()
+        with patch.object(MockCursor, 'execute', Mock(side_effect=ProgrammingError)):
+            self.assertRaises(ProgrammingError, self.c.bootstrap)
+        with patch.object(MockCursor, 'execute', Mock(side_effect=[ProgrammingError, None, None, None])), \
+                patch.object(ProgrammingError, 'diag') as mock_diag:
+            type(mock_diag).sqlstate = PropertyMock(return_value='42P04')
+            self.c.bootstrap()
         mock_logger.assert_called_once()
         self.assertTrue(mock_logger.call_args[0][0].startswith('Exception when creating database'))