Skip to content

Commit ff27870

Browse files
authored
Still check against `postgres --describe-config` if a GUC does not have a validator but is a valid postgres GUC
1 parent 74c0acf commit ff27870

File tree

8 files changed

+89
-25
lines changed

8 files changed

+89
-25
lines changed

patroni/postgresql/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from .. import global_config, psycopg
1919
from ..async_executor import CriticalTask
20-
from ..collections import CaseInsensitiveDict, EMPTY_DICT
20+
from ..collections import CaseInsensitiveDict, CaseInsensitiveSet, EMPTY_DICT
2121
from ..dcs import Cluster, Leader, Member, slot_name_from_member_name
2222
from ..exceptions import PostgresConnectionException
2323
from ..tags import Tags
@@ -119,6 +119,8 @@ def __init__(self, config: Dict[str, Any], mpp: AbstractMPP) -> None:
119119
# Last known running process
120120
self._postmaster_proc = None
121121

122+
self._available_gucs = None
123+
122124
if self.is_running():
123125
# If we found postmaster process we need to figure out whether postgres is accepting connections
124126
self.set_state('starting')
@@ -243,6 +245,13 @@ def cluster_info_query(self) -> str:
243245

244246
return ("SELECT " + self.TL_LSN + ", {3}").format(self.wal_name, self.lsn_name, self.wal_flush, extra)
245247

248+
@property
249+
def available_gucs(self) -> CaseInsensitiveSet:
250+
"""GUCs available in this Postgres server."""
251+
if not self._available_gucs:
252+
self._available_gucs = self._get_gucs()
253+
return self._available_gucs
254+
246255
def _version_file_exists(self) -> bool:
247256
return not self.data_directory_empty() and os.path.isfile(self._version_file)
248257

@@ -646,6 +655,7 @@ def is_running(self) -> Optional[PostmasterProcess]:
646655
if self._postmaster_proc.is_running():
647656
return self._postmaster_proc
648657
self._postmaster_proc = None
658+
self._available_gucs = None
649659

650660
# we noticed that postgres was restarted, force syncing of replication slots and check of logical slots
651661
self.slots_handler.schedule()
@@ -1365,3 +1375,13 @@ def schedule_sanity_checks_after_pause(self) -> None:
13651375
self.slots_handler.schedule()
13661376
self.mpp_handler.schedule_cache_rebuild()
13671377
self._sysid = ''
1378+
1379+
def _get_gucs(self) -> CaseInsensitiveSet:
1380+
"""Get all available GUCs based on ``postgres --describe-config`` output.
1381+
1382+
:returns: all available GUCs in the local Postgres server.
1383+
"""
1384+
cmd = [self.pgcommand('postgres'), '--describe-config']
1385+
return CaseInsensitiveSet({
1386+
line.split('\t')[0] for line in subprocess.check_output(cmd).decode('utf-8').strip().split('\n')
1387+
})

patroni/postgresql/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ def write_postgresql_conf(self, configuration: Optional[CaseInsensitiveDict] = N
511511
f.writeline("include '{0}'\n".format(ConfigWriter.escape(include)))
512512
version = self.pg_version
513513
for name, value in sorted((configuration).items()):
514-
value = transform_postgresql_parameter_value(version, name, value)
514+
value = transform_postgresql_parameter_value(version, name, value, self._postgresql.available_gucs)
515515
if value is not None and\
516516
(name != 'hba_file' or not self._postgresql.bootstrap.running_custom_bootstrap):
517517
f.write_param(name, value)
@@ -634,7 +634,8 @@ def _write_recovery_params(self, fd: ConfigWriter, recovery_params: CaseInsensit
634634
self._passfile_mtime = mtime(self._pgpass)
635635
value = self.format_dsn(value)
636636
else:
637-
value = transform_recovery_parameter_value(self._postgresql.major_version, name, value)
637+
value = transform_recovery_parameter_value(self._postgresql.major_version, name, value,
638+
self._postgresql.available_gucs)
638639
if value is None:
639640
continue
640641
fd.write_param(name, value)

patroni/postgresql/validator.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import yaml
88

9-
from ..collections import CaseInsensitiveDict
9+
from ..collections import CaseInsensitiveDict, CaseInsensitiveSet
1010
from ..exceptions import PatroniException
1111
from ..utils import parse_bool, parse_int, parse_real
1212
from .available_parameters import get_validator_files, PathLikeObj
@@ -412,8 +412,9 @@ class in this module.
412412

413413

414414
def _transform_parameter_value(validators: MutableMapping[str, Tuple[_Transformable, ...]],
415-
version: int, name: str, value: Any) -> Optional[Any]:
416-
"""Validate *value* of GUC *name* for Postgres *version* using defined *validators*.
415+
version: int, name: str, value: Any,
416+
available_gucs: CaseInsensitiveSet) -> Optional[Any]:
417+
"""Validate *value* of GUC *name* for Postgres *version* using defined *validators* and *available_gucs*.
417418
418419
:param validators: a dictionary of all GUCs across all Postgres versions. Each key is the name of a Postgres GUC,
419420
and the corresponding value is a variable length tuple of :class:`_Transformable`. Each item is a validation
@@ -422,30 +423,37 @@ def _transform_parameter_value(validators: MutableMapping[str, Tuple[_Transforma
422423
:param version: Postgres version to validate the GUC against.
423424
:param name: name of the Postgres GUC.
424425
:param value: value of the Postgres GUC.
425-
426-
* Disallow writing GUCs to ``postgresql.conf`` (or ``recovery.conf``) that does not exist in Postgres *version*;
427-
* Avoid ignoring GUC *name* if it does not have a validator in *validators*, but is a valid GUC in Postgres
428-
*version*.
426+
:param available_gucs: a set of all GUCs available in Postgres *version*. Each item is the name of a Postgres
427+
GUC. Used to avoid ignoring GUC *name* if it does not have a validator in *validators*, but is a valid GUC
428+
in Postgres *version*.
429429
430430
:returns: the return value may be one among:
431431
432432
* *value* transformed to the expected format for GUC *name* in Postgres *version*, if *name* has a validator
433433
in *validators* for the corresponding Postgres *version*; or
434-
* ``None`` if *name* does not have a validator in *validators*.
434+
* ``None`` if *name* does not have a validator in *validators* and is not present in *available_gucs*.
435435
"""
436436
for validator in validators.get(name, ()) or ():
437437
if version >= validator.version_from and\
438438
(validator.version_till is None or version < validator.version_till):
439439
return validator.transform(name, value)
440+
# Ideally we should have a validator in *validators*. However, if none is available, we will not discard a
441+
# setting that exists in Postgres *version*, but rather allow the value with no validation.
442+
if name in available_gucs:
443+
return value
440444
logger.warning('Removing unexpected parameter=%s value=%s from the config', name, value)
441445

442446

443-
def transform_postgresql_parameter_value(version: int, name: str, value: Any) -> Optional[Any]:
444-
"""Validate *value* of GUC *name* for Postgres *version* using ``parameters``.
447+
def transform_postgresql_parameter_value(version: int, name: str, value: Any,
448+
available_gucs: CaseInsensitiveSet) -> Optional[Any]:
449+
"""Validate *value* of GUC *name* for Postgres *version* using ``parameters`` and *available_gucs*.
445450
446451
:param version: Postgres version to validate the GUC against.
447452
:param name: name of the Postgres GUC.
448453
:param value: value of the Postgres GUC.
454+
:param available_gucs: a set of all GUCs available in Postgres *version*. Each item is the name of a Postgres
455+
GUC. Used to avoid ignoring GUC *name* if it does not have a validator in ``parameters``, but is a valid GUC in
456+
Postgres *version*.
449457
450458
:returns: The return value may be one among:
451459
@@ -460,17 +468,28 @@ def transform_postgresql_parameter_value(version: int, name: str, value: Any) ->
460468
return value
461469
if name in recovery_parameters:
462470
return None
463-
return _transform_parameter_value(parameters, version, name, value)
471+
return _transform_parameter_value(parameters, version, name, value, available_gucs)
464472

465473

466-
def transform_recovery_parameter_value(version: int, name: str, value: Any) -> Optional[Any]:
467-
"""Validate *value* of GUC *name* for Postgres *version* using ``recovery_parameters``.
474+
def transform_recovery_parameter_value(version: int, name: str, value: Any,
475+
available_gucs: CaseInsensitiveSet) -> Optional[Any]:
476+
"""Validate *value* of GUC *name* for Postgres *version* using ``recovery_parameters`` and *available_gucs*.
468477
469478
:param version: Postgres version to validate the recovery GUC against.
470479
:param name: name of the Postgres recovery GUC.
471480
:param value: value of the Postgres recovery GUC.
481+
:param available_gucs: a set of all GUCs available in Postgres *version*. Each item is the name of a Postgres
482+
GUC. Used to avoid ignoring GUC *name* if it does not have a validator in ``parameters``, but is a valid GUC in
483+
Postgres *version*.
472484
473485
:returns: *value* transformed to the expected format for recovery GUC *name* in Postgres *version* using validators
474486
defined in ``recovery_parameters``. It can also return ``None``. See :func:`_transform_parameter_value`.
475487
"""
476-
return _transform_parameter_value(recovery_parameters, version, name, value)
488+
# Recovery settings are not present in ``postgres --describe-config`` output of Postgres <= 11. In that case we
489+
# just pass down the list of settings defined in Patroni validators so :func:`_transform_parameter_value` will not
490+
# discard the recovery GUCs when running Postgres <= 11.
491+
# NOTE: At the moment this change was done Postgres 11 was almost EOL, and had been likely extensively used with
492+
# Patroni, so we should be able to rely solely on Patroni validators as the source of truth.
493+
return _transform_parameter_value(
494+
recovery_parameters, version, name, value,
495+
available_gucs if version >= 120000 else CaseInsensitiveSet(recovery_parameters.keys()))

tests/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import shutil
44
import unittest
55

6-
from unittest.mock import Mock, patch
6+
from unittest.mock import Mock, patch, PropertyMock
77

88
import urllib3
99

@@ -20,6 +20,15 @@ class SleepException(Exception):
2020
pass
2121

2222

23+
mock_available_gucs = PropertyMock(return_value={
24+
'cluster_name', 'constraint_exclusion', 'force_parallel_mode', 'hot_standby', 'listen_addresses', 'max_connections',
25+
'max_locks_per_transaction', 'max_prepared_transactions', 'max_replication_slots', 'max_stack_depth',
26+
'max_wal_senders', 'max_worker_processes', 'port', 'search_path', 'shared_preload_libraries',
27+
'stats_temp_directory', 'synchronous_standby_names', 'track_commit_timestamp', 'unix_socket_directories',
28+
'vacuum_cost_delay', 'vacuum_cost_limit', 'wal_keep_size', 'wal_level', 'wal_log_hints', 'zero_damaged_pages',
29+
'autovacuum', 'wal_segment_size', 'wal_block_size', 'shared_buffers', 'wal_buffers', 'fork_specific_param',
30+
})
31+
2332
GET_PG_SETTINGS_RESULT = [
2433
('wal_segment_size', '2048', '8kB', 'integer', 'internal'),
2534
('wal_block_size', '8192', None, 'integer', 'internal'),

tests/test_bootstrap.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
from patroni.postgresql.cancellable import CancellableSubprocess
1111
from patroni.postgresql.config import ConfigHandler, get_param_diff
1212

13-
from . import BaseTestPostgresql, psycopg_connect
13+
from . import BaseTestPostgresql, mock_available_gucs, psycopg_connect
1414

1515

1616
@patch('subprocess.call', Mock(return_value=0))
1717
@patch('subprocess.check_output', Mock(return_value=b"postgres (PostgreSQL) 12.1"))
1818
@patch('patroni.psycopg.connect', psycopg_connect)
1919
@patch('os.rename', Mock())
20+
@patch.object(Postgresql, 'available_gucs', mock_available_gucs)
2021
class TestBootstrap(BaseTestPostgresql):
2122

2223
@patch('patroni.postgresql.CallbackExecutor', Mock())

tests/test_patroni.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def test_validate_config(self):
7878
@patch.object(etcd.Client, 'read', etcd_read)
7979
@patch.object(Thread, 'start', Mock())
8080
@patch.object(AbstractEtcdClientWithFailover, '_get_machines_list', Mock(return_value=['http://remotehost:2379']))
81+
@patch.object(Postgresql, '_get_gucs', Mock(return_value={'foo': True, 'bar': True}))
8182
def setUp(self):
8283
self._handlers = logging.getLogger().handlers[:]
8384
RestApiServer._BaseServer__is_shut_down = Mock()
@@ -111,6 +112,7 @@ def test_apply_dynamic_configuration(self):
111112
@patch.object(etcd.Client, 'delete', Mock())
112113
@patch.object(AbstractEtcdClientWithFailover, '_get_machines_list', Mock(return_value=['http://remotehost:2379']))
113114
@patch.object(Thread, 'join', Mock())
115+
@patch.object(Postgresql, '_get_gucs', Mock(return_value={'foo': True, 'bar': True}))
114116
def test_patroni_patroni_main(self):
115117
with patch('subprocess.call', Mock(return_value=1)):
116118
with patch.object(Patroni, 'run', Mock(side_effect=SleepException)):

tests/test_postgresql.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from patroni import global_config
1717
from patroni.async_executor import CriticalTask
18-
from patroni.collections import CaseInsensitiveDict
18+
from patroni.collections import CaseInsensitiveDict, CaseInsensitiveSet
1919
from patroni.dcs import RemoteMember
2020
from patroni.exceptions import PatroniException, PostgresConnectionException
2121
from patroni.postgresql import Postgresql, STATE_NO_RESPONSE, STATE_REJECT
@@ -29,7 +29,8 @@
2929
ValidatorFactoryInvalidType, ValidatorFactoryNoType
3030
from patroni.utils import RetryFailedError
3131

32-
from . import BaseTestPostgresql, GET_PG_SETTINGS_RESULT, MockCursor, MockPostmaster, psycopg_connect
32+
from . import BaseTestPostgresql, GET_PG_SETTINGS_RESULT, \
33+
mock_available_gucs, MockCursor, MockPostmaster, psycopg_connect
3334

3435
mtime_ret = {}
3536

@@ -100,13 +101,15 @@ def pg_controldata_string(*args, **kwargs):
100101
@patch('subprocess.call', Mock(return_value=0))
101102
@patch('subprocess.check_output', Mock(return_value=b"postgres (PostgreSQL) 12.1"))
102103
@patch('patroni.psycopg.connect', psycopg_connect)
104+
@patch.object(Postgresql, 'available_gucs', mock_available_gucs)
103105
class TestPostgresql(BaseTestPostgresql):
104106

105107
@patch('subprocess.call', Mock(return_value=0))
106108
@patch('os.rename', Mock())
107109
@patch('patroni.postgresql.CallbackExecutor', Mock())
108110
@patch.object(Postgresql, 'get_major_version', Mock(return_value=140000))
109111
@patch.object(Postgresql, 'is_running', Mock(return_value=True))
112+
@patch.object(Postgresql, 'available_gucs', mock_available_gucs)
110113
def setUp(self):
111114
super(TestPostgresql, self).setUp()
112115
self.p.config.write_postgresql_conf()
@@ -912,11 +915,12 @@ def test_transform_postgresql_parameter_value(self, mock_warning):
912915
not_none_values = (
913916
('foo.bar', 'foo', 160003), # name, value, version
914917
("allow_in_place_tablespaces", 'true', 130008),
915-
("restrict_nonsystem_relation_kind", 'view', 160005)
918+
("restrict_nonsystem_relation_kind", 'view', 160005),
919+
('fork_specific_param', 'no_validation_file', 170001),
916920
)
917921
for i in not_none_values:
918922
self.assertIsNotNone(
919-
transform_postgresql_parameter_value(i[2], i[0], i[1])
923+
transform_postgresql_parameter_value(i[2], i[0], i[1], mock_available_gucs.return_value)
920924
)
921925

922926
none_values = (
@@ -926,7 +930,7 @@ def test_transform_postgresql_parameter_value(self, mock_warning):
926930
)
927931
for i in none_values:
928932
self.assertIsNone(
929-
transform_postgresql_parameter_value(i[2], i[0], i[1])
933+
transform_postgresql_parameter_value(i[2], i[0], i[1], mock_available_gucs.return_value)
930934
)
931935
if i[3]:
932936
mock_warning.assert_called_once_with(
@@ -1155,6 +1159,12 @@ class TestPostgresql2(BaseTestPostgresql):
11551159
def setUp(self):
11561160
super(TestPostgresql2, self).setUp()
11571161

1162+
@patch('subprocess.check_output', Mock(return_value='\n'.join(mock_available_gucs.return_value).encode('utf-8')))
1163+
def test_available_gucs(self):
1164+
gucs = self.p.available_gucs
1165+
self.assertIsInstance(gucs, CaseInsensitiveSet)
1166+
self.assertEqual(gucs, mock_available_gucs.return_value)
1167+
11581168
def test_cluster_info_query(self):
11591169
self.assertIn('diff(pg_catalog.pg_current_wal_flush_lsn(', self.p.cluster_info_query)
11601170
self.p._major_version = 90600

tests/test_sync.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
from patroni.dcs import Cluster, ClusterConfig, Status, SyncState
88
from patroni.postgresql import Postgresql
99

10-
from . import BaseTestPostgresql, psycopg_connect
10+
from . import BaseTestPostgresql, mock_available_gucs, psycopg_connect
1111

1212

1313
@patch('subprocess.call', Mock(return_value=0))
1414
@patch('patroni.psycopg.connect', psycopg_connect)
15+
@patch.object(Postgresql, 'available_gucs', mock_available_gucs)
1516
class TestSync(BaseTestPostgresql):
1617

1718
@patch('subprocess.call', Mock(return_value=0))
1819
@patch('os.rename', Mock())
1920
@patch('patroni.postgresql.CallbackExecutor', Mock())
2021
@patch.object(Postgresql, 'get_major_version', Mock(return_value=140000))
2122
@patch.object(Postgresql, 'is_running', Mock(return_value=True))
23+
@patch.object(Postgresql, 'available_gucs', mock_available_gucs)
2224
def setUp(self):
2325
super(TestSync, self).setUp()
2426
self.p.config.write_postgresql_conf()

0 commit comments

Comments
 (0)