Release v3.2.2 (#3007)

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

Co-authored-by: Polina Bungina <[email protected]>
This commit is contained in:
Alexander Kukushkin
2024-01-17 08:35:35 +01:00
co-authored by Polina Bungina
parent f2919f9c2f
commit c8e32775df
8 changed files with 69 additions and 15 deletions
+1 -1
View File
@@ -174,7 +174,7 @@ jobs:
- uses: jakebailey/pyright-action@v1 - uses: jakebailey/pyright-action@v1
with: with:
version: 1.1.338 version: 1.1.347
docs: docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+50
View File
@@ -3,6 +3,56 @@
Release notes 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 Version 3.2.1
------------- -------------
+2 -2
View File
@@ -1418,7 +1418,7 @@ def switchover(obj: Dict[str, Any], cluster_name: str, group: Optional[int],
def generate_topology(level: int, member: Dict[str, Any], 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. """Recursively yield members with their names adjusted according to their *level* in the cluster topology.
.. note:: .. note::
@@ -1481,7 +1481,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 :yields: *members* sorted by level in the topology, and with a new ``name`` value according to their level
in the topology. 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}) 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')) replicas = set(member['name'] for member in members if not member['role'].endswith('leader'))
for member in members: for member in members:
+6 -3
View File
@@ -7,7 +7,7 @@ from urllib.parse import urlparse
from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING
from ..dcs import CITUS_COORDINATOR_GROUP_ID, Cluster from ..dcs import CITUS_COORDINATOR_GROUP_ID, Cluster
from ..psycopg import connect, quote_ident, DuplicateDatabase from ..psycopg import connect, quote_ident, ProgrammingError
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from . import Postgresql from . import Postgresql
@@ -364,8 +364,11 @@ class CitusHandler(Thread):
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute('CREATE DATABASE {0}'.format( cur.execute('CREATE DATABASE {0}'.format(
quote_ident(self._config['database'], conn)).encode('utf-8')) quote_ident(self._config['database'], conn)).encode('utf-8'))
except DuplicateDatabase as e: except ProgrammingError as exc:
logger.debug('Exception when creating database: %r', e) if exc.diag.sqlstate == '42P04': # DuplicateDatabase
logger.debug('Exception when creating database: %r', exc)
else:
raise exc
finally: finally:
conn.close() conn.close()
+1 -4
View File
@@ -9,8 +9,7 @@ if TYPE_CHECKING: # pragma: no cover
from psycopg import Connection from psycopg import Connection
from psycopg2 import connection, cursor from psycopg2 import connection, cursor
__all__ = ['connect', 'quote_ident', 'quote_literal', 'DatabaseError', 'Error', 'OperationalError', 'ProgrammingError', __all__ = ['connect', 'quote_ident', 'quote_literal', 'DatabaseError', 'Error', 'OperationalError', 'ProgrammingError']
'DuplicateDatabase']
_legacy = False _legacy = False
try: try:
@@ -19,7 +18,6 @@ try:
if parse_version(__version__) < MIN_PSYCOPG2: if parse_version(__version__) < MIN_PSYCOPG2:
raise ImportError raise ImportError
from psycopg2 import connect as _connect, Error, DatabaseError, OperationalError, ProgrammingError from psycopg2 import connect as _connect, Error, DatabaseError, OperationalError, ProgrammingError
from psycopg2.errors import DuplicateDatabase
from psycopg2.extensions import adapt from psycopg2.extensions import adapt
try: try:
@@ -45,7 +43,6 @@ try:
return value.getquoted().decode('utf-8') return value.getquoted().decode('utf-8')
except ImportError: except ImportError:
from psycopg import connect as __connect, sql, Error, DatabaseError, OperationalError, ProgrammingError 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]': def _connect(dsn: Optional[str] = None, **kwargs: Any) -> 'Connection[Any]':
"""Call :func:`psycopg.connect` with *dsn* and ``**kwargs``. """Call :func:`psycopg.connect` with *dsn* and ``**kwargs``.
+1 -1
View File
@@ -2,4 +2,4 @@
:var __version__: the current Patroni version. :var __version__: the current Patroni version.
""" """
__version__ = '3.2.1' __version__ = '3.2.2'
-2
View File
@@ -128,8 +128,6 @@ class MockCursor(object):
sql = sql.decode('utf-8') sql = sql.decode('utf-8')
if sql.startswith('blabla'): if sql.startswith('blabla'):
raise psycopg.ProgrammingError() raise psycopg.ProgrammingError()
if sql.startswith('CREATE DATABASE'):
raise psycopg.DuplicateDatabase()
elif sql == 'CHECKPOINT' or sql.startswith('SELECT pg_catalog.pg_create_'): elif sql == 'CHECKPOINT' or sql.startswith('SELECT pg_catalog.pg_create_'):
raise psycopg.OperationalError() raise psycopg.OperationalError()
elif sql.startswith('RetryFailedError'): elif sql.startswith('RetryFailedError'):
+7 -1
View File
@@ -1,6 +1,7 @@
import time import time
from mock import Mock, patch from mock import Mock, patch, PropertyMock
from patroni.postgresql.citus import CitusHandler from patroni.postgresql.citus import CitusHandler
from patroni.psycopg import ProgrammingError
from . import BaseTestPostgresql, MockCursor, psycopg_connect, SleepException from . import BaseTestPostgresql, MockCursor, psycopg_connect, SleepException
from .test_ha import get_cluster_initialized_with_leader from .test_ha import get_cluster_initialized_with_leader
@@ -166,6 +167,11 @@ class TestCitus(BaseTestPostgresql):
@patch('patroni.postgresql.citus.connect', psycopg_connect) @patch('patroni.postgresql.citus.connect', psycopg_connect)
@patch('patroni.postgresql.citus.quote_ident', Mock()) @patch('patroni.postgresql.citus.quote_ident', Mock())
def test_bootstrap_duplicate_database(self, mock_logger): def test_bootstrap_duplicate_database(self, mock_logger):
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() self.c.bootstrap()
mock_logger.assert_called_once() mock_logger.assert_called_once()
self.assertTrue(mock_logger.call_args[0][0].startswith('Exception when creating database')) self.assertTrue(mock_logger.call_args[0][0].startswith('Exception when creating database'))