diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index aafe6b56..aac111c1 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/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 3981bcae..c9aec6de 100644 --- a/patroni/ctl.py +++ b/patroni/ctl.py @@ -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], - 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:: @@ -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 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/citus.py b/patroni/postgresql/citus.py index ca7e4cd0..3a56d6bd 100644 --- a/patroni/postgresql/citus.py +++ b/patroni/postgresql/citus.py @@ -7,7 +7,7 @@ from urllib.parse import urlparse from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING 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 from . import Postgresql @@ -364,8 +364,11 @@ class CitusHandler(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' diff --git a/tests/__init__.py b/tests/__init__.py index 0b83a4df..0d71d505 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -128,8 +128,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 6ac039fe..8c241fb2 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.citus import CitusHandler +from patroni.psycopg import ProgrammingError from . import BaseTestPostgresql, MockCursor, psycopg_connect, SleepException 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.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'))