Compare commits

...
20 Commits
Author SHA1 Message Date
Alexander KukushkinandGitHub dc1966e3bc Release 1.6.4 (#1380)
* Bump version
* Update release notes
2020-01-27 14:15:21 +01:00
Alexander KukushkinandGitHub 6aa3f809d4 Configure keepalive for connections to K8s API (#1366)
In case if we got nothing from the socket after the TTL seconds it should be considered dead.
2020-01-27 09:25:08 +01:00
Alexander KukushkinandGitHub 902411239f More compatibility with windows (#1367)
* unix-domain sockets are not yet supported
* signal.SIGQUIT doesn't exists
2020-01-24 12:52:55 +01:00
Alexander KukushkinandGitHub 0eb1e0568b Compatibility with python 3.7.6 (#1368)
The urlparse function has changed.

Old versions:
```python
>>> urlparse('localhost:8500')
ParseResult(scheme='', netloc='', path='localhost:8500', params='', query='', fragment='')
```

3.7.6:
```python
>>> urlparse('localhost:8500')
ParseResult(scheme='localhost', netloc='', path='8500', params='', query='', fragment='')
```
2020-01-24 12:52:34 +01:00
Alexander KukushkinandGitHub 27ff9dfda3 Avoid logging passwords on user creation (#1370)
Fixes https://github.com/zalando/patroni/issues/1365
2020-01-24 10:52:23 +01:00
Igor YanchenkoandAlexander Kukushkin 16fe180ed6 implemented stop signal using pg_ctl for non posix systems (#1342)
Using pg_ctl to send stop signal for non posix os.
2020-01-16 14:35:47 +01:00
Alexander KukushkinandGitHub 1c4d395d5a Handle exception from Ha.shutdown (#1351)
During the shutdown Patroni is trying to update its status in the DCS.
If the DCS is inaccessible an exception might be raised. Lack of exception handling prevents logger thread from stopping.

Fixes https://github.com/zalando/patroni/issues/1344
2020-01-16 14:34:58 +01:00
Alexander KukushkinandGitHub c5fc6fd936 Special handling of parameters with period (#1349)
It might be that they are defined by the extension and therefore the unit is not necessarily is the string.
It also could be that change of the value requires a restart (for example pg_stat_statements.max).
2020-01-16 14:34:31 +01:00
Alexander KukushkinandGitHub 1461d7d4b8 Allow certain recovery parameters be defined in the custom_conf (#1335)
Fixes https://github.com/zalando/patroni/issues/1333
2020-01-15 12:41:07 +01:00
Igor YanchenkoandAlexander Kukushkin ea76a40845 Make sure postgresql.pgpass is a file or it does not exist (#1337)
Also make sure that it is located in the writable directory.
2020-01-15 12:40:41 +01:00
Alexander KukushkinandGitHub 102a12ea5a Catch all exceptions when calling socket.getaddrinfo (#1355)
it serves two purposes:
1. We don't want accidentally break the thread
2. During the shutdown socket.gaierror become unresolvable and nasty exceptions are raised

Close: https://github.com/zalando/patroni/issues/1353
2020-01-15 12:38:34 +01:00
Alexander KukushkinandGitHub 9e8ae95bce Finally fix a problem with case sensitivity of sync standby names (#1359)
Close https://github.com/zalando/patroni/issues/1358
2020-01-15 12:38:05 +01:00
Kostiantyn NemchenkoandAlexander Kukushkin a2a5cc2f71 Disable serfHealth Consul check (#1364)
Fixes #1362 and #1363.
2020-01-15 12:37:35 +01:00
Alexander KukushkinandGitHub 16d1ffdde7 Update timeline on standby cluster (#1332)
Fixes https://github.com/zalando/patroni/issues/1031
2019-12-20 12:56:00 +01:00
Alexander KukushkinandGitHub a675fa18dc Handle case when replication password is set to null (#1330)
Fixes https://github.com/zalando/patroni/issues/1231
2019-12-20 12:06:18 +01:00
Igor YanchenkoandAlexander Kukushkin 26b6e00575 wait option for patronictl reinit implemented (#1339)
Wait to finish `reinit` if `--wait` option is used.
Every 2 seconds it pulls the status from Patroni REST API and reports to console.
2019-12-20 12:05:39 +01:00
Alexander KukushkinandGitHub d941f6bc5e Use restore_command from standby_cluster config on cascading replicas (#1341)
The standby_leader was already doing it from the beginning feature existed. Not doing the same on replicas might prevent them from catching up with standby leader due to WALs being recycled.

In addition to that apply the same strategy to archive_cleanup_command.
2019-12-20 12:03:25 +01:00
Igor YanchenkoandAlexander Kukushkin 7ff27d9e10 Make sure unix_socket_directories and stats_temp_directory exist (#1293)
Upon the start of Patroni and Postgres make sure that unix_socket_directories and stats_temp_directory exist or try to create them. Patroni will exit if failed to create them.

Close https://github.com/zalando/patroni/issues/863
2019-12-11 12:26:17 +01:00
Igor YanchenkoandAlexander Kukushkin 2174d66f97 Rewriten shell scripts in python to make them compatible with windows (#1326) 2019-12-11 12:07:05 +01:00
Pavlo GolubandAlexander Kukushkin 919e9c54d2 Make dest argument default value of backup() cross platform (#1324)
Fixes #1325
2019-12-11 11:25:41 +01:00
37 changed files with 452 additions and 157 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ Consul
- **PATRONI\_CONSUL\_KEY**: (optional) File with the client key. Can be empty if the key is part of certificate. - **PATRONI\_CONSUL\_KEY**: (optional) File with the client key. Can be empty if the key is part of certificate.
- **PATRONI\_CONSUL\_DC**: (optional) Datacenter to communicate with. By default the datacenter of the host is used. - **PATRONI\_CONSUL\_DC**: (optional) Datacenter to communicate with. By default the datacenter of the host is used.
- **PATRONI\_CONSUL\_CONSISTENCY**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__) - **PATRONI\_CONSUL\_CONSISTENCY**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__)
- **PATRONI\_CONSUL\_CHECKS**: (optional) list of Consul health checks used for the session. If not specified Consul will use "serfHealth" in additional to the TTL based check created by Patroni. Additional checks, in particular the "serfHealth", may cause the leader lock to expire faster than in `ttl` seconds when the leader instance becomes unavailable. - **PATRONI\_CONSUL\_CHECKS**: (optional) list of Consul health checks used for the session. By default an empty list is used.
- **PATRONI\_CONSUL\_REGISTER\_SERVICE**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, replica or standby-leader depending on the node's role. Defaults to **false** - **PATRONI\_CONSUL\_REGISTER\_SERVICE**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, replica or standby-leader depending on the node's role. Defaults to **false**
- **PATRONI\_CONSUL\_SERVICE\_CHECK\_INTERVAL**: (optional) how often to perform health check against registered url - **PATRONI\_CONSUL\_SERVICE\_CHECK\_INTERVAL**: (optional) how often to perform health check against registered url
+1 -1
View File
@@ -98,7 +98,7 @@ Most of the parameters are optional, but you have to specify one of the **host**
- **key**: (optional) file with the client key. Can be empty if the key is part of **cert**. - **key**: (optional) file with the client key. Can be empty if the key is part of **cert**.
- **dc**: (optional) Datacenter to communicate with. By default the datacenter of the host is used. - **dc**: (optional) Datacenter to communicate with. By default the datacenter of the host is used.
- **consistency**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__) - **consistency**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__)
- **checks**: (optional) list of Consul health checks used for the session. If not specified Consul will use "serfHealth" in additional to the TTL based check created by Patroni. Additional checks, in particular the "serfHealth", may cause the leader lock to expire faster than in `ttl` seconds when the leader instance becomes unavailable - **checks**: (optional) list of Consul health checks used for the session. By default an empty list is used.
- **register\_service**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, replica or standby-leader depending on the node's role. Defaults to **false** - **register\_service**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, replica or standby-leader depending on the node's role. Defaults to **false**
- **service\_check\_interval**: (optional) how often to perform health check against registered url - **service\_check\_interval**: (optional) how often to perform health check against registered url
+62
View File
@@ -3,6 +3,68 @@
Release notes Release notes
============= =============
Version 1.6.4
-------------
**New features**
- Implemented ``--wait`` option for ``patronictl reinit`` (Igor Yanchenko)
Patronictl will wait for ``reinit`` to finish is the ``--wait`` option is used.
- Further improvements of Windows support (Igor Yanchenko, Alexander Kukushkin)
1. All shell scripts which are used for integration testing are rewritten in python
2. The ``pg_ctl kill`` will be used to stop postgres on non posix systems
3. Don't try to use unix-domain sockets
**Stability improvements**
- Make sure ``unix_socket_directories`` and ``stats_temp_directory`` exist (Igor)
Upon the start of Patroni and Postgres make sure that ``unix_socket_directories`` and ``stats_temp_directory`` exist or try to create them. Patroni will exit if failed to create them.
- Make sure ``postgresql.pgpass`` is located in the place where Patroni has write access (Igor)
In case if it doesn't have a write access Patroni will exit with exception.
- Disable Consul ``serfHealth`` check by default (Kostiantyn Nemchenko)
Even in case of little network problems the failing ``serfHealth`` leads to invalidation of all sessions associated with the node. Therefore, the leader key is lost much earlier than ``ttl`` which causes unwanted restarts of replicas and maybe demotion of the primary.
- Configure tcp keepalives for connections to K8s API (Alexander)
In case if we get nothing from the socket after TTL seconds it can be considered dead.
- Avoid logging of passwords on user creation (Alexander)
If the password is rejected or logging is configured to verbose or not configured at all it might happen that the password is written into postgres logs. In order to avoid it Patroni will change ``log_statement``, ``log_min_duration_statement``, and ``log_min_error_statement`` to some safe values before doing the attempt to create/update user.
**Bugfixes**
- Use ``restore_command`` from the ``standby_cluster`` config on cascading replicas (Alexander)
The ``standby_leader`` was already doing it from the beginning the feature existed. Not doing the same on replicas might prevent them from catching up with standby leader.
- Update timeline reported by the standby cluster (Alexander)
In case of timeline switch the standby cluster was correctly replicating from the primary but ``patronictl`` was reporting the old timeline.
- Allow certain recovery parameters be defined in the custom_conf (Alexander)
When doing validation of recovery parameters on replica Patroni will skip ``archive_cleanup_command``, ``promote_trigger_file``, ``recovery_end_command``, ``recovery_min_apply_delay``, and ``restore_command`` if they are not defined in the patroni config but in files other than ``postgresql.auto.conf`` or ``postgresql.conf``.
- Improve handling of postgresql parameters with period in its name (Alexander)
Such parameters could be defined by extensions where the unit is not necessarily a string. Changing the value might require a restart (for example ``pg_stat_statements.max``).
- Improve exception handling during shutdown (Alexander)
During shutdown Patroni is trying to update its status in the DCS. If the DCS is inaccessible an exception might be raised. Lack of exception handling was preventing logger thread from stopping.
Version 1.6.3 Version 1.6.3
------------- -------------
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python
import os
import argparse
import shutil
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--dirname", required=True)
parser.add_argument("--pathname", required=True)
parser.add_argument("--filename", required=True)
parser.add_argument("--mode", required=True, choices=("archive", "restore"))
args, _ = parser.parse_known_args()
full_filename = os.path.join(args.dirname, args.filename)
if args.mode == "archive":
if not os.path.isdir(args.dirname):
os.makedirs(args.dirname)
if not os.path.exists(full_filename):
shutil.copy(args.pathname, full_filename)
else:
shutil.copy(full_filename, args.pathname)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python
import argparse
import subprocess
import sys
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--datadir", required=True)
parser.add_argument("--dbname", required=True)
parser.add_argument("--walmethod", required=True, choices=("fetch", "stream", "none"))
args, _ = parser.parse_known_args()
walmethod = ["-X", args.walmethod] if args.walmethod != "none" else []
sys.exit(subprocess.call(["pg_basebackup", "-D", args.datadir, "-c", "fast", "-d", args.dbname] + walmethod))
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
while getopts ":-:" optchar; do
[[ "${optchar}" == "-" ]] || continue
case "${OPTARG}" in
datadir=* )
PGDATA=${OPTARG#*=}
;;
dbname=* )
DBNAME=${OPTARG#*=}
;;
walmethod=* )
WALMETHOD=${OPTARG#*=}
;;
esac
done
[[ -z $PGDATA || -z $DBNAME || -z $WALMETHOD ]] && exit 1
[[ $WALMETHOD != "none" ]] && WALMETHOD="-X $WALMETHOD" || WALMETHOD=""
exec pg_basebackup -D $PGDATA $WALMETHOD -c fast -d $DBNAME
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env python
import argparse
import shutil
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--datadir", required=True)
parser.add_argument("--sourcedir", required=True)
args, _ = parser.parse_known_args()
shutil.copytree(args.sourcedir, args.datadir)
-21
View File
@@ -1,21 +0,0 @@
#!/bin/bash
set -x
while getopts ":-:" optchar; do
[[ "${optchar}" == "-" ]] || continue
case "${OPTARG}" in
datadir=* )
PGDATA=${OPTARG#*=}
;;
sourcedir=* )
SOURCE=${OPTARG#*=}
;;
esac
done
[[ -z $PGDATA || -z $SOURCE ]] && exit 1
mkdir -p $(dirname $PGDATA)
exec cp -af $SOURCE $PGDATA
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env python
import os
import psycopg2
import sys
if __name__ == '__main__':
if not (len(sys.argv) >= 3 and sys.argv[3] == "master"):
sys.exit(1)
os.environ['PGPASSWORD'] = 'zalando'
connection = psycopg2.connect(host='127.0.0.1', port=sys.argv[1], user='postgres')
cursor = connection.cursor()
cursor.execute("SELECT slot_name FROM pg_replication_slots WHERE slot_type = 'logical'")
with open("data/postgres0/label", "w") as label:
label.write(next(iter(cursor.fetchone()), ""))
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
[[ "$3" == "master" ]] || exit
PGPASSWORD=zalando psql -h localhost -U postgres -p $1 -w -tAc "SELECT slot_name FROM pg_replication_slots WHERE slot_type = 'logical'" >> data/postgres0/label
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python
import sys
with open("data/{0}/{0}_cb.log".format(sys.argv[1]), "a+") as log:
log.write(" ".join(sys.argv[-3:]) + "\n")
+17 -12
View File
@@ -171,7 +171,8 @@ class PatroniController(AbstractController):
config['name'] = name config['name'] = name
config['postgresql']['data_dir'] = self._data_dir config['postgresql']['data_dir'] = self._data_dir
config['postgresql']['use_unix_socket'] = True config['postgresql']['use_unix_socket'] = os.name != 'nt' # windows doesn't yet support unix-domain sockets
config['postgresql']['pgpass'] = os.path.join(tempfile.gettempdir(), 'pgpass_' + name)
config['postgresql']['parameters'].update({ config['postgresql']['parameters'].update({
'logging_collector': 'on', 'log_destination': 'csvlog', 'log_directory': self._output_dir, 'logging_collector': 'on', 'log_destination': 'csvlog', 'log_directory': self._output_dir,
'log_filename': name + '.log', 'log_statement': 'all', 'log_min_messages': 'debug1', 'log_filename': name + '.log', 'log_statement': 'all', 'log_min_messages': 'debug1',
@@ -258,10 +259,10 @@ class PatroniController(AbstractController):
def backup_source(self): def backup_source(self):
return 'postgres://{username}:{password}@{host}:{port}/{database}'.format(**self._replication) return 'postgres://{username}:{password}@{host}:{port}/{database}'.format(**self._replication)
def backup(self, dest='data/basebackup'): def backup(self, dest=os.path.join('data', 'basebackup')):
subprocess.call([PatroniPoolController.BACKUP_SCRIPT, '--walmethod=none', subprocess.call(PatroniPoolController.BACKUP_SCRIPT + ['--walmethod=none',
'--datadir=' + os.path.join(self._work_directory, dest), '--datadir=' + os.path.join(self._work_directory, dest),
'--dbname=' + self.backup_source]) '--dbname=' + self.backup_source])
class ProcessHang(object): class ProcessHang(object):
@@ -532,7 +533,8 @@ class ExhibitorController(ZooKeeperController):
class PatroniPoolController(object): class PatroniPoolController(object):
BACKUP_SCRIPT = 'features/backup_create.sh' BACKUP_SCRIPT = [sys.executable, 'features/backup_create.py']
ARCHIVE_RESTORE_SCRIPT = ' '.join((sys.executable, os.path.abspath('features/archive-restore.py')))
def __init__(self, context): def __init__(self, context):
self._context = context self._context = context
@@ -593,7 +595,7 @@ class PatroniPoolController(object):
'bootstrap': { 'bootstrap': {
'method': 'pg_basebackup', 'method': 'pg_basebackup',
'pg_basebackup': { 'pg_basebackup': {
'command': self.BACKUP_SCRIPT + ' --walmethod=stream --dbname=' + f.backup_source 'command': " ".join(self.BACKUP_SCRIPT) + ' --walmethod=stream --dbname=' + f.backup_source
}, },
'dcs': { 'dcs': {
'postgresql': { 'postgresql': {
@@ -606,8 +608,9 @@ class PatroniPoolController(object):
'postgresql': { 'postgresql': {
'parameters': { 'parameters': {
'archive_mode': 'on', 'archive_mode': 'on',
'archive_command': 'mkdir -p {0} && test ! -f {0}/%f && cp %p {0}/%f'.format( 'archive_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode archive ' +
os.path.join(self.patroni_path, 'data', 'wal_archive')) '--dirname {} --filename %f --pathname %p').format(
os.path.join(self.patroni_path, 'data', 'wal_archive'))
}, },
'authentication': { 'authentication': {
'superuser': {'password': 'zalando1'}, 'superuser': {'password': 'zalando1'},
@@ -623,12 +626,14 @@ class PatroniPoolController(object):
'bootstrap': { 'bootstrap': {
'method': 'backup_restore', 'method': 'backup_restore',
'backup_restore': { 'backup_restore': {
'command': 'features/backup_restore.sh --sourcedir=' + os.path.join(self.patroni_path, 'command': (sys.executable + ' features/backup_restore.py --sourcedir=' +
'data', 'basebackup'), os.path.join(self.patroni_path, 'data', 'basebackup')),
'recovery_conf': { 'recovery_conf': {
'recovery_target_action': 'promote', 'recovery_target_action': 'promote',
'recovery_target_timeline': 'latest', 'recovery_target_timeline': 'latest',
'restore_command': 'cp {0}/data/wal_archive/%f %p'.format(self.patroni_path) 'restore_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode restore ' +
'--dirname {} --filename %f --pathname %p').format(
os.path.join(self.patroni_path, 'data', 'wal_archive'))
} }
} }
}, },
+1 -1
View File
@@ -30,7 +30,7 @@ Feature: standby cluster
When I issue a GET request to http://127.0.0.1:8009/standby_leader When I issue a GET request to http://127.0.0.1:8009/standby_leader
Then I receive a response code 200 Then I receive a response code 200
And I receive a response role standby_leader And I receive a response role standby_leader
And there is a postgres1_cb.log with "on_start replica batman1\non_role_change standby_leader batman1" in postgres1 data directory And there is a postgres1_cb.log with "on_role_change standby_leader batman1" in postgres1 data directory
When I start postgres2 in a cluster batman1 When I start postgres2 in a cluster batman1
Then postgres2 role is the replica after 24 seconds Then postgres2 role is the replica after 24 seconds
And table foo is present on postgres2 after 20 seconds And table foo is present on postgres2 after 20 seconds
+1 -1
View File
@@ -13,7 +13,7 @@ def start_patroni_with_a_name_value_tag(context, name, tag_name, tag_value):
def check_label(context, label, content, name): def check_label(context, label, content, name):
label = context.pctl.read_label(name, label) label = context.pctl.read_label(name, label)
label = label.replace('\n', '\\n') label = label.replace('\n', '\\n')
assert label == content, "{0} is not equal to {1}".format(label, content) assert content in label, "{0} doesn't contain {1}".format(label, content)
@step('I create label with "{content:w}" in {name:w} data directory') @step('I create label with "{content:w}" in {name:w} data directory')
+5 -4
View File
@@ -1,4 +1,5 @@
import os import os
import sys
import time import time
from behave import step from behave import step
@@ -9,7 +10,7 @@ SELECT * FROM pg_catalog.pg_stat_replication
WHERE application_name = '{0}' WHERE application_name = '{0}'
""" """
callback = "bash -c 'echo \"${*: -3:1} ${*: -2:1} ${*: -1:1}\" >> data/$1/$1_cb.log' -- " callback = sys.executable + " features/callback2.py "
@step('I start {name:w} with callback configured') @step('I start {name:w} with callback configured')
@@ -17,7 +18,7 @@ def start_patroni_with_callbacks(context, name):
return context.pctl.start(name, custom_config={ return context.pctl.start(name, custom_config={
"postgresql": { "postgresql": {
"callbacks": { "callbacks": {
"on_role_change": "features/callback.sh" "on_role_change": sys.executable + " features/callback.py"
} }
} }
}) })
@@ -30,8 +31,8 @@ def start_patroni(context, name, cluster_name):
"postgresql": { "postgresql": {
"callbacks": {c: callback + name for c in ('on_start', 'on_stop', 'on_restart', 'on_role_change')}, "callbacks": {c: callback + name for c in ('on_start', 'on_stop', 'on_restart', 'on_role_change')},
"backup_restore": { "backup_restore": {
"command": "features/backup_restore.sh --sourcedir=" + os.path.join(context.pctl.patroni_path, "command": (sys.executable + " features/backup_restore.py --sourcedir=" +
'data', 'basebackup')} os.path.join(context.pctl.patroni_path, 'data', 'basebackup'))}
} }
}) })
+4 -1
View File
@@ -159,7 +159,10 @@ class Patroni(object):
self.api.shutdown() self.api.shutdown()
except Exception: except Exception:
logger.exception('Exception during RestApi.shutdown') logger.exception('Exception during RestApi.shutdown')
self.ha.shutdown() try:
self.ha.shutdown()
except Exception:
logger.exception('Exception during Ha.shutdown')
self.logger.shutdown() self.logger.shutdown()
+23 -4
View File
@@ -49,11 +49,11 @@ class PatroniCtlException(ClickException):
def parse_dcs(dcs): def parse_dcs(dcs):
if dcs is None: if dcs is None:
return None return None
elif '//' not in dcs:
dcs = '//' + dcs
parsed = urlparse(dcs) parsed = urlparse(dcs)
scheme = parsed.scheme scheme = parsed.scheme
if scheme == '' and parsed.netloc == '':
parsed = urlparse('//' + dcs)
port = int(parsed.port) if parsed.port else None port = int(parsed.port) if parsed.port else None
if scheme == '': if scheme == '':
@@ -558,20 +558,39 @@ def restart(obj, cluster_name, member_names, force, role, p_any, scheduled, vers
@click.argument('cluster_name') @click.argument('cluster_name')
@click.argument('member_names', nargs=-1) @click.argument('member_names', nargs=-1)
@option_force @option_force
@click.option('--wait', help='Wait until reinitialization completes', is_flag=True)
@click.pass_obj @click.pass_obj
def reinit(obj, cluster_name, member_names, force): def reinit(obj, cluster_name, member_names, force, wait):
cluster = get_dcs(obj, cluster_name).get_cluster() cluster = get_dcs(obj, cluster_name).get_cluster()
members = get_members(cluster, cluster_name, member_names, None, force, 'reinitialize') members = get_members(cluster, cluster_name, member_names, None, force, 'reinitialize')
wait_on_members = []
for member in members: for member in members:
body = {'force': force} body = {'force': force}
while True: while True:
r = request_patroni(member, 'post', 'reinitialize', body) r = request_patroni(member, 'post', 'reinitialize', body)
if not check_response(r, member.name, 'reinitialize') and r.data.endswith(b' already in progress') \ started = check_response(r, member.name, 'reinitialize')
if not started and r.data.endswith(b' already in progress') \
and not force and click.confirm('Do you want to cancel it and reinitialize anyway?'): and not force and click.confirm('Do you want to cancel it and reinitialize anyway?'):
body['force'] = True body['force'] = True
continue continue
break break
if started and wait:
wait_on_members.append(member)
last_display = []
while wait_on_members:
if wait_on_members != last_display:
click.echo('Waiting for reinitialize to complete on: {0}'.format(
", ".join(member.name for member in wait_on_members))
)
last_display[:] = wait_on_members
time.sleep(2)
for member in wait_on_members:
data = json.loads(request_patroni(member, 'get', 'patroni').data.decode('utf-8'))
if data.get('state') != 'creating replica':
click.echo('Reinitialize is completed on: {0}'.format(member.name))
wait_on_members.remove(member)
def _do_failover_or_switchover(obj, action, cluster_name, master, candidate, force, scheduled=None): def _do_failover_or_switchover(obj, action, cluster_name, master, candidate, force, scheduled=None):
+8 -4
View File
@@ -167,7 +167,7 @@ class Member(namedtuple('Member', 'index,name,session,data')):
# apply any remaining authentication parameters # apply any remaining authentication parameters
if auth and isinstance(auth, dict): if auth and isinstance(auth, dict):
ret.update(auth) ret.update({k: v for k, v in auth.items() if v is not None})
if 'username' in auth: if 'username' in auth:
ret['user'] = ret.pop('username') ret['user'] = ret.pop('username')
return ret return ret
@@ -240,21 +240,25 @@ class Leader(namedtuple('Leader', 'index,session,member')):
def conn_url(self): def conn_url(self):
return self.member.conn_url return self.member.conn_url
@property
def data(self):
return self.member.data
@property @property
def timeline(self): def timeline(self):
return self.member.data.get('timeline') return self.data.get('timeline')
@property @property
def checkpoint_after_promote(self): def checkpoint_after_promote(self):
""" """
>>> Leader(1, '', Member.from_node(1, '', '', '{"version":"z"}')).checkpoint_after_promote >>> Leader(1, '', Member.from_node(1, '', '', '{"version":"z"}')).checkpoint_after_promote
""" """
version = self.member.data.get('version') version = self.data.get('version')
if version: if version:
try: try:
# 1.5.6 is the last version which doesn't expose checkpoint_after_promote: false # 1.5.6 is the last version which doesn't expose checkpoint_after_promote: false
if tuple(map(int, version.split('.'))) > (1, 5, 6): if tuple(map(int, version.split('.'))) > (1, 5, 6):
return self.member.data['role'] == 'master' and 'checkpoint_after_promote' not in self.member.data return self.data['role'] == 'master' and 'checkpoint_after_promote' not in self.data
except Exception: except Exception:
logger.debug('Failed to parse Patroni version %s', version) logger.debug('Failed to parse Patroni version %s', version)
+1 -1
View File
@@ -217,7 +217,7 @@ class Consul(AbstractDCS):
self.set_retry_timeout(config['retry_timeout']) self.set_retry_timeout(config['retry_timeout'])
self.set_ttl(config.get('ttl') or 30) self.set_ttl(config.get('ttl') or 30)
self._last_session_refresh = 0 self._last_session_refresh = 0
self.__session_checks = config.get('checks') self.__session_checks = config.get('checks', [])
self._register_service = config.get('register_service', False) self._register_service = config.get('register_service', False)
if self._register_service: if self._register_service:
self._service_name = service_name_from_scope_name(self._scope) self._service_name = service_name_from_scope_name(self._scope)
+2 -2
View File
@@ -73,8 +73,8 @@ class DnsCachingResolver(Thread):
def _do_resolve(host, port): def _do_resolve(host, port):
try: try:
return socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP) return socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
except socket.gaierror: except Exception as e:
logger.warning('failed to resolve host %s', host) logger.warning('failed to resolve host %s: %s', host, e)
return [] return []
+21 -5
View File
@@ -41,8 +41,22 @@ class CoreV1ApiProxy(object):
self._request_timeout = None self._request_timeout = None
self._use_endpoints = use_endpoints self._use_endpoints = use_endpoints
def set_timeout(self, timeout): def configure_timeouts(self, loop_wait, retry_timeout, ttl):
self._request_timeout = (1, timeout / 3.0) # Normally every loop_wait seconds we should have receive something from the socket.
# If we didn't received anything after the loop_wait + retry_timeout it is a time
# to start worrying (send keepalive messages). Finally, the connection should be
# considered as dead if we received nothing from the socket after the ttl seconds.
cnt = 3
idle = int(loop_wait + retry_timeout)
intvl = max(1, int(float(ttl - idle) / cnt))
self._api.api_client.rest_client.pool_manager.connection_pool_kw['socket_options'] = [
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, idle),
(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl),
(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt),
(socket.IPPROTO_TCP, 18, int(ttl * 1000)) # TCP_USER_TIMEOUT
]
self._request_timeout = (1, retry_timeout / 3.0)
def __getattr__(self, func): def __getattr__(self, func):
if func.endswith('_kind'): if func.endswith('_kind'):
@@ -210,8 +224,7 @@ class Kubernetes(AbstractDCS):
self.__subsets = [k8s_client.V1EndpointSubset(addresses=addresses, ports=ports)] self.__subsets = [k8s_client.V1EndpointSubset(addresses=addresses, ports=ports)]
self._should_create_config_service = True self._should_create_config_service = True
self._api = CoreV1ApiProxy(use_endpoints) self._api = CoreV1ApiProxy(use_endpoints)
self.set_retry_timeout(config['retry_timeout']) self.reload_config(config)
self.set_ttl(config.get('ttl') or 30)
self._leader_observed_record = {} self._leader_observed_record = {}
self._leader_observed_time = None self._leader_observed_time = None
self._leader_resource_version = None self._leader_resource_version = None
@@ -250,7 +263,10 @@ class Kubernetes(AbstractDCS):
def set_retry_timeout(self, retry_timeout): def set_retry_timeout(self, retry_timeout):
self._retry.deadline = retry_timeout self._retry.deadline = retry_timeout
self._api.set_timeout(retry_timeout)
def reload_config(self, config):
super(Kubernetes, self).reload_config(config)
self._api.configure_timeouts(self.loop_wait, self._retry.deadline, self.ttl)
@staticmethod @staticmethod
def member(pod): def member(pod):
+29 -6
View File
@@ -191,10 +191,20 @@ class Ha(object):
if self._async_executor.scheduled_action in (None, 'promote') \ if self._async_executor.scheduled_action in (None, 'promote') \
and data['state'] in ['running', 'restarting', 'starting']: and data['state'] in ['running', 'restarting', 'starting']:
try: try:
timeline, wal_position = self.state_handler.timeline_wal_position() timeline, wal_position, pg_control_timeline = self.state_handler.timeline_wal_position()
data['xlog_location'] = wal_position data['xlog_location'] = wal_position
if not timeline: if not timeline:
timeline = self.state_handler.replica_cached_timeline(self._leader_timeline) # So far the only way to get the current timeline on the standby is from
# the replication connection. In order to avoid opening the replication
# connection on every iteration of HA loop we will do it only when noticed
# that the timeline on the primary has changed.
# Unfortunately such optimization isn't possible on the standby_leader,
# therefore we will get the timeline from pg_control, either by calling
# pg_control_checkpoint() on 9.6+ or by parsing the output of pg_controldata.
if self.state_handler.role == 'standby_leader':
timeline = pg_control_timeline or self.state_handler.pg_control_timeline()
else:
timeline = self.state_handler.replica_cached_timeline(timeline)
if timeline: if timeline:
data['timeline'] = timeline data['timeline'] = timeline
except Exception: except Exception:
@@ -342,14 +352,26 @@ class Ha(object):
def _get_node_to_follow(self, cluster): def _get_node_to_follow(self, cluster):
# determine the node to follow. If replicatefrom tag is set, # determine the node to follow. If replicatefrom tag is set,
# try to follow the node mentioned there, otherwise, follow the leader. # try to follow the node mentioned there, otherwise, follow the leader.
if self.is_standby_cluster() and (self.cluster.is_unlocked() or self.has_lock(False)): standby_config = self.get_standby_cluster_config()
is_standby_cluster = _is_standby_cluster(standby_config)
if is_standby_cluster and (self.cluster.is_unlocked() or self.has_lock(False)):
node_to_follow = self.get_remote_master() node_to_follow = self.get_remote_master()
elif self.patroni.replicatefrom and self.patroni.replicatefrom != self.state_handler.name: elif self.patroni.replicatefrom and self.patroni.replicatefrom != self.state_handler.name:
node_to_follow = cluster.get_member(self.patroni.replicatefrom) node_to_follow = cluster.get_member(self.patroni.replicatefrom)
else: else:
node_to_follow = cluster.leader node_to_follow = cluster.leader
return node_to_follow if node_to_follow and node_to_follow.name != self.state_handler.name else None node_to_follow = node_to_follow if node_to_follow and node_to_follow.name != self.state_handler.name else None
if node_to_follow and not isinstance(node_to_follow, RemoteMember):
# we are going to abuse Member.data to pass following parameters
params = ('restore_command', 'archive_cleanup_command')
for param in params: # It is highly unlikely to happen, but we want to protect from the case
node_to_follow.data.pop(param, None) # when above-mentioned params came from outside.
if is_standby_cluster:
node_to_follow.data.update({p: standby_config[p] for p in params if standby_config.get(p)})
return node_to_follow
def follow(self, demote_reason, follow_reason, refresh=True): def follow(self, demote_reason, follow_reason, refresh=True):
if refresh: if refresh:
@@ -585,7 +607,7 @@ class Ha(object):
"""This method tries to determine whether I am healthy enough to became a new leader candidate or not.""" """This method tries to determine whether I am healthy enough to became a new leader candidate or not."""
# We don't call `last_operation()` here because it returns a string # We don't call `last_operation()` here because it returns a string
_, my_wal_position = self.state_handler.timeline_wal_position() _, my_wal_position, _ = self.state_handler.timeline_wal_position()
if check_replication_lag and self.is_lagging(my_wal_position): if check_replication_lag and self.is_lagging(my_wal_position):
logger.info('My wal position exceeds maximum replication lag') logger.info('My wal position exceeds maximum replication lag')
return False # Too far behind last reported wal position on master return False # Too far behind last reported wal position on master
@@ -1268,7 +1290,8 @@ class Ha(object):
data_sysid = self.state_handler.sysid data_sysid = self.state_handler.sysid
if not self.sysid_valid(data_sysid): if not self.sysid_valid(data_sysid):
# data directory is not empty, but no valid sysid, cluster must be broken, suggest reinit # data directory is not empty, but no valid sysid, cluster must be broken, suggest reinit
return "data dir for the cluster is not empty, but system ID is invalid; consider doing reinitalize" return ("data dir for the cluster is not empty, but system ID is invalid; consider doing"
"reinitialize")
if self.sysid_valid(self.cluster.initialize): if self.sysid_valid(self.cluster.initialize):
if self.cluster.initialize != data_sysid: if self.cluster.initialize != data_sysid:
+20 -9
View File
@@ -60,6 +60,7 @@ class Postgresql(object):
self._pending_restart = False self._pending_restart = False
self._connection = Connection() self._connection = Connection()
self.config = ConfigHandler(self, config) self.config = ConfigHandler(self, config)
self.config.check_directories()
self._bin_dir = config.get('bin_dir') or '' self._bin_dir = config.get('bin_dir') or ''
self.bootstrap = Bootstrap(self) self.bootstrap = Bootstrap(self)
@@ -133,6 +134,8 @@ class Postgresql(object):
@property @property
def cluster_info_query(self): def cluster_info_query(self):
pg_control_timeline = 'timeline_id FROM pg_catalog.pg_control_checkpoint()' \
if self._major_version >= 90600 and self.role == 'standby_leader' else '0'
return ("SELECT CASE WHEN pg_catalog.pg_is_in_recovery() THEN 0 " return ("SELECT CASE WHEN pg_catalog.pg_is_in_recovery() THEN 0 "
"ELSE ('x' || pg_catalog.substr(pg_catalog.pg_{0}file_name(" "ELSE ('x' || pg_catalog.substr(pg_catalog.pg_{0}file_name("
"pg_catalog.pg_current_{0}_{1}()), 1, 8))::bit(32)::int END, " "pg_catalog.pg_current_{0}_{1}()), 1, 8))::bit(32)::int END, "
@@ -141,7 +144,7 @@ class Postgresql(object):
"pg_catalog.pg_last_{0}_receive_{1}(), '0/0'), '0/0')::bigint," "pg_catalog.pg_last_{0}_receive_{1}(), '0/0'), '0/0')::bigint,"
" pg_catalog.pg_{0}_{1}_diff(pg_catalog.pg_last_{0}_replay_{1}(), '0/0')::bigint)" " pg_catalog.pg_{0}_{1}_diff(pg_catalog.pg_last_{0}_replay_{1}(), '0/0')::bigint)"
"ELSE pg_catalog.pg_{0}_{1}_diff(pg_catalog.pg_current_{0}_{1}(), '0/0')::bigint " "ELSE pg_catalog.pg_{0}_{1}_diff(pg_catalog.pg_current_{0}_{1}(), '0/0')::bigint "
"END").format(self.wal_name, self.lsn_name) "END, {2}").format(self.wal_name, self.lsn_name, pg_control_timeline)
def _version_file_exists(self): def _version_file_exists(self):
return not self.data_directory_empty() and os.path.isfile(self._version_file) return not self.data_directory_empty() and os.path.isfile(self._version_file)
@@ -288,7 +291,7 @@ class Postgresql(object):
if not self._cluster_info_state: if not self._cluster_info_state:
try: try:
result = self._is_leader_retry(self._query, self.cluster_info_query).fetchone() result = self._is_leader_retry(self._query, self.cluster_info_query).fetchone()
self._cluster_info_state = dict(zip(['timeline', 'wal_position'], result)) self._cluster_info_state = dict(zip(['timeline', 'wal_position', 'pg_control_timeline'], result))
except RetryFailedError as e: # SELECT failed two times except RetryFailedError as e: # SELECT failed two times
self._cluster_info_state = {'error': str(e)} self._cluster_info_state = {'error': str(e)}
if not self.is_starting() and self.pg_isready() == STATE_REJECT: if not self.is_starting() and self.pg_isready() == STATE_REJECT:
@@ -302,6 +305,12 @@ class Postgresql(object):
def is_leader(self): def is_leader(self):
return bool(self._cluster_info_state_get('timeline')) return bool(self._cluster_info_state_get('timeline'))
def pg_control_timeline(self):
try:
return int(self.controldata().get("Latest checkpoint's TimeLineID"))
except (TypeError, ValueError):
logger.exception('Failed to parse timeline from pg_controldata output')
def is_running(self): def is_running(self):
"""Returns PostmasterProcess if one is running on the data directory or None. If most recently seen process """Returns PostmasterProcess if one is running on the data directory or None. If most recently seen process
is running updates the cached process based on pid file.""" is running updates the cached process based on pid file."""
@@ -407,6 +416,7 @@ class Postgresql(object):
self._pending_restart = False self._pending_restart = False
configuration = self.config.effective_configuration configuration = self.config.effective_configuration
self.config.check_directories()
self.config.write_postgresql_conf(configuration) self.config.write_postgresql_conf(configuration)
self.config.resolve_connection_addresses() self.config.resolve_connection_addresses()
self.config.replace_pg_hba() self.config.replace_pg_hba()
@@ -506,7 +516,7 @@ class Postgresql(object):
self.set_state('stopping') self.set_state('stopping')
# Send signal to postmaster to stop # Send signal to postmaster to stop
success = postmaster.signal_stop(mode) success = postmaster.signal_stop(mode, self.pgcommand('pg_ctl'))
if success is not None: if success is not None:
if success and on_safepoint: if success and on_safepoint:
on_safepoint() on_safepoint()
@@ -523,11 +533,10 @@ class Postgresql(object):
return True, True return True, True
@staticmethod def terminate_starting_postmaster(self, postmaster):
def terminate_starting_postmaster(postmaster):
"""Terminates a postmaster that has not yet opened ports or possibly even written a pid file. Blocks """Terminates a postmaster that has not yet opened ports or possibly even written a pid file. Blocks
until the process goes away.""" until the process goes away."""
postmaster.signal_stop('immediate') postmaster.signal_stop('immediate', self.pgcommand('pg_ctl'))
postmaster.wait() postmaster.wait()
def _wait_for_connection_close(self, postmaster): def _wait_for_connection_close(self, postmaster):
@@ -733,11 +742,13 @@ class Postgresql(object):
# This method could be called from different threads (simultaneously with some other `_query` calls). # This method could be called from different threads (simultaneously with some other `_query` calls).
# If it is called not from main thread we will create a new cursor to execute statement. # If it is called not from main thread we will create a new cursor to execute statement.
if current_thread().ident == self.__thread_ident: if current_thread().ident == self.__thread_ident:
return self._cluster_info_state_get('timeline'), self._cluster_info_state_get('wal_position') return (self._cluster_info_state_get('timeline'),
self._cluster_info_state_get('wal_position'),
self._cluster_info_state_get('pg_control_timeline'))
with self.connection().cursor() as cursor: with self.connection().cursor() as cursor:
cursor.execute(self.cluster_info_query) cursor.execute(self.cluster_info_query)
return cursor.fetchone()[:2] return cursor.fetchone()[:3]
def postmaster_start_time(self): def postmaster_start_time(self):
try: try:
@@ -815,7 +826,7 @@ class Postgresql(object):
if state != 'streaming' or not member or member.tags.get('nosync', False): if state != 'streaming' or not member or member.tags.get('nosync', False):
continue continue
if sync_state == 'sync': if sync_state == 'sync':
return app_name, True return member.name, True
if sync_state == 'potential' and app_name == current: if sync_state == 'potential' and app_name == current:
# Prefer current even if not the best one any more to avoid indecisivness and spurious swaps. # Prefer current even if not the best one any more to avoid indecisivness and spurious swaps.
return cluster.sync.sync_standby, False return cluster.sync.sync_standby, False
+9 -1
View File
@@ -310,7 +310,15 @@ BEGIN
CREATE ROLE "{0}" WITH {1}; CREATE ROLE "{0}" WITH {1};
END IF; END IF;
END;$$""".format(name, ' '.join(options)) END;$$""".format(name, ' '.join(options))
self._postgresql.query(sql, *params) self._postgresql.query('SET log_statement TO none')
self._postgresql.query('SET log_min_duration_statement TO -1')
self._postgresql.query("SET log_min_error_statement TO 'log'")
try:
self._postgresql.query(sql, *params)
finally:
self._postgresql.query('RESET log_min_error_statement')
self._postgresql.query('RESET log_min_duration_statement')
self._postgresql.query('RESET log_statement')
def post_bootstrap(self, config, task): def post_bootstrap(self, config, task):
try: try:
+49 -26
View File
@@ -6,11 +6,13 @@ import socket
import stat import stat
import time import time
from patroni.exceptions import PatroniException
from six.moves.urllib_parse import urlparse, parse_qsl, unquote from six.moves.urllib_parse import urlparse, parse_qsl, unquote
from urllib3.response import HTTPHeaderDict from urllib3.response import HTTPHeaderDict
from ..dcs import slot_name_from_member_name, RemoteMember from ..dcs import slot_name_from_member_name, RemoteMember
from ..utils import compare_values, parse_bool, parse_int, split_host_port, uri from ..utils import compare_values, parse_bool, parse_int, split_host_port, uri, \
validate_directory, is_subpath
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -333,7 +335,10 @@ class ConfigHandler(object):
self._standby_signal = os.path.join(postgresql.data_dir, 'standby.signal') self._standby_signal = os.path.join(postgresql.data_dir, 'standby.signal')
self._auto_conf = os.path.join(postgresql.data_dir, 'postgresql.auto.conf') self._auto_conf = os.path.join(postgresql.data_dir, 'postgresql.auto.conf')
self._auto_conf_mtime = None self._auto_conf_mtime = None
self._pgpass = config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass') self._pgpass = os.path.abspath(config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass'))
if os.path.exists(self._pgpass) and not os.path.isfile(self._pgpass):
raise PatroniException("'{}' exists and it's not a file, check your `postgresql.pgpass` configuration"
.format(self._pgpass))
self._passfile = None self._passfile = None
self._passfile_mtime = None self._passfile_mtime = None
self._synchronous_standby_names = None self._synchronous_standby_names = None
@@ -347,6 +352,21 @@ class ConfigHandler(object):
self._server_parameters = self.get_server_parameters(self._config) self._server_parameters = self.get_server_parameters(self._config)
self._adjust_recovery_parameters() self._adjust_recovery_parameters()
def try_to_create_dir(self, d, msg):
d = os.path.join(self._postgresql._data_dir, d)
if (not is_subpath(self._postgresql._data_dir, d) or not self._postgresql.data_directory_empty()):
validate_directory(d, msg)
def check_directories(self):
if "unix_socket_directories" in self._server_parameters:
for d in self._server_parameters["unix_socket_directories"].split(","):
self.try_to_create_dir(d.strip(), "'{}' is defined in unix_socket_directories, {}")
if "stats_temp_directory" in self._server_parameters:
self.try_to_create_dir(self._server_parameters["stats_temp_directory"],
"'{}' is defined in stats_temp_directory, {}")
self.try_to_create_dir(os.path.dirname(self._pgpass),
"'{}' is defined in `postgresql.pgpass`, {}")
@property @property
def _configuration_to_save(self): def _configuration_to_save(self):
configuration = [os.path.basename(self._postgresql_conf)] configuration = [os.path.basename(self._postgresql_conf)]
@@ -436,7 +456,7 @@ class ConfigHandler(object):
# when we are doing custom bootstrap we assume that we don't know superuser password # when we are doing custom bootstrap we assume that we don't know superuser password
# and in order to be able to change it, we are opening trust access from a certain address # and in order to be able to change it, we are opening trust access from a certain address
if self._postgresql.bootstrap.running_custom_bootstrap: if self._postgresql.bootstrap.running_custom_bootstrap:
addresses = {'': 'local'} addresses = {} if os.name == 'nt' else {'': 'local'} # windows doesn't yet support unix-domain sockets
if 'host' in self.local_replication_address and not self.local_replication_address['host'].startswith('/'): if 'host' in self.local_replication_address and not self.local_replication_address['host'].startswith('/'):
addresses.update({sa[0] + '/32': 'host' for _, _, _, _, sa in socket.getaddrinfo( addresses.update({sa[0] + '/32': 'host' for _, _, _, _, sa in socket.getaddrinfo(
self.local_replication_address['host'], self.local_replication_address['port'], self.local_replication_address['host'], self.local_replication_address['port'],
@@ -521,15 +541,16 @@ class ConfigHandler(object):
is_remote_master = isinstance(member, RemoteMember) is_remote_master = isinstance(member, RemoteMember)
primary_conninfo = self.primary_conninfo_params(member) primary_conninfo = self.primary_conninfo_params(member)
if primary_conninfo: if primary_conninfo:
use_slots = self.get('use_slots', True) and self._postgresql.major_version >= 90400
if use_slots and not (is_remote_master and member.no_replication_slot):
primary_slot_name = member.primary_slot_name if is_remote_master else self._postgresql.name
recovery_params['primary_slot_name'] = slot_name_from_member_name(primary_slot_name)
recovery_params['primary_conninfo'] = primary_conninfo recovery_params['primary_conninfo'] = primary_conninfo
if self.get('use_slots', True) and self._postgresql.major_version >= 90400 \
and not (is_remote_master and member.no_replication_slot):
recovery_params['primary_slot_name'] = member.primary_slot_name if is_remote_master \
else slot_name_from_member_name(self._postgresql.name)
if is_remote_master: # standby_cluster config might have different parameters, we want to override them # standby_cluster config might have different parameters, we want to override them
recovery_params.update({p: member.data.get(p) for p in ('restore_command', 'recovery_min_apply_delay', standby_cluster_params = ['restore_command', 'archive_cleanup_command']\
'archive_cleanup_command') if member.data.get(p)}) + (['recovery_min_apply_delay'] if is_remote_master else [])
recovery_params.update({p: member.data.get(p) for p in standby_cluster_params if member and member.data.get(p)})
return recovery_params return recovery_params
def recovery_conf_exists(self): def recovery_conf_exists(self):
@@ -564,7 +585,7 @@ class ConfigHandler(object):
try: try:
values = self._get_pg_settings(self._recovery_parameters_to_compare).values() values = self._get_pg_settings(self._recovery_parameters_to_compare).values()
values = {p[0]: [p[1], p[4] == 'postmaster'] for p in values} values = {p[0]: [p[1], p[4] == 'postmaster', p[5]] for p in values}
self._postgresql_conf_mtime = pg_conf_mtime self._postgresql_conf_mtime = pg_conf_mtime
self._auto_conf_mtime = auto_conf_mtime self._auto_conf_mtime = auto_conf_mtime
self._postmaster_ctime = postmaster_ctime self._postmaster_ctime = postmaster_ctime
@@ -673,6 +694,12 @@ class ConfigHandler(object):
wanted_recovery_params = self.build_recovery_params(member) wanted_recovery_params = self.build_recovery_params(member)
for param, value in self._current_recovery_params.items(): for param, value in self._current_recovery_params.items():
# Skip certain parameters defined in the included postgres config files
# if we know that they are not specified in the patroni configuration.
if len(value) > 2 and value[2] not in (self._postgresql_conf, self._auto_conf) and \
param in ('archive_cleanup_command', 'promote_trigger_file', 'recovery_end_command',
'recovery_min_apply_delay', 'restore_command') and param not in wanted_recovery_params:
continue
if param == 'recovery_min_apply_delay': if param == 'recovery_min_apply_delay':
if not compare_values('integer', 'ms', value[0], wanted_recovery_params.get(param, 0)): if not compare_values('integer', 'ms', value[0], wanted_recovery_params.get(param, 0)):
record_missmatch(value[1]) record_missmatch(value[1])
@@ -842,7 +869,7 @@ class ConfigHandler(object):
self._postgresql.set_connection_kwargs(self.local_connect_kwargs) self._postgresql.set_connection_kwargs(self.local_connect_kwargs)
def _get_pg_settings(self, names): def _get_pg_settings(self, names):
return {r[0]: r for r in self._postgresql.query(('SELECT name, setting, unit, vartype, context ' return {r[0]: r for r in self._postgresql.query(('SELECT name, setting, unit, vartype, context, sourcefile'
+ ' FROM pg_catalog.pg_settings ' + + ' FROM pg_catalog.pg_settings ' +
' WHERE pg_catalog.lower(name) = ANY(%s)'), ' WHERE pg_catalog.lower(name) = ANY(%s)'),
[n.lower() for n in names])} [n.lower() for n in names])}
@@ -871,9 +898,9 @@ class ConfigHandler(object):
conf_changed = hba_changed = ident_changed = local_connection_address_changed = pending_restart = False conf_changed = hba_changed = ident_changed = local_connection_address_changed = pending_restart = False
if self._postgresql.state == 'running': if self._postgresql.state == 'running':
changes = CaseInsensitiveDict({p: v for p, v in server_parameters.items() changes = CaseInsensitiveDict({p: v for p, v in server_parameters.items()
if '.' not in p and p.lower() not in self._RECOVERY_PARAMETERS}) if p.lower() not in self._RECOVERY_PARAMETERS})
changes.update({p: None for p in self._server_parameters.keys() changes.update({p: None for p in self._server_parameters.keys()
if not ('.' in p or p in changes or p.lower() in self._RECOVERY_PARAMETERS)}) if not (p in changes or p.lower() in self._RECOVERY_PARAMETERS)})
if changes: if changes:
if 'wal_buffers' in changes: # we need to calculate the default value of wal_buffers if 'wal_buffers' in changes: # we need to calculate the default value of wal_buffers
undef = [p for p in ('shared_buffers', 'wal_segment_size', 'wal_block_size') if p not in changes] undef = [p for p in ('shared_buffers', 'wal_segment_size', 'wal_block_size') if p not in changes]
@@ -899,21 +926,17 @@ class ConfigHandler(object):
local_connection_address_changed = True local_connection_address_changed = True
else: else:
logger.info('Changed %s from %s to %s', r[0], r[1], new_value) logger.info('Changed %s from %s to %s', r[0], r[1], new_value)
for param in changes: for param, value in changes.items():
if param in server_parameters: if '.' in param:
# Check that user-defined-paramters have changed (parameters with period in name)
if value is None or param not in self._server_parameters \
or str(value) != str(self._server_parameters[param]):
logger.info('Changed %s from %s to %s', param, self._server_parameters.get(param), value)
conf_changed = True
elif param in server_parameters:
logger.warning('Removing invalid parameter `%s` from postgresql.parameters', param) logger.warning('Removing invalid parameter `%s` from postgresql.parameters', param)
server_parameters.pop(param) server_parameters.pop(param)
# Check that user-defined-paramters have changed (parameters with period in name)
for p, v in server_parameters.items():
if '.' in p and (p not in self._server_parameters or str(v) != str(self._server_parameters[p])):
logger.info('Changed %s from %s to %s', p, self._server_parameters.get(p), v)
conf_changed = True
for p, v in self._server_parameters.items():
if '.' in p and (p not in server_parameters or str(v) != str(server_parameters[p])):
logger.info('Changed %s from %s to %s', p, v, server_parameters.get(p))
conf_changed = True
if not server_parameters.get('hba_file') and config.get('pg_hba'): if not server_parameters.get('hba_file') and config.get('pg_hba'):
hba_changed = self._config.get('pg_hba', []) != config['pg_hba'] hba_changed = self._config.get('pg_hba', []) != config['pg_hba']
+17 -5
View File
@@ -20,9 +20,9 @@ elif sys.version_info >= (3, 4): # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
STOP_SIGNALS = { STOP_SIGNALS = {
'smart': signal.SIGTERM, 'smart': 'TERM',
'fast': signal.SIGINT, 'fast': 'INT',
'immediate': signal.SIGQUIT if os.name != 'nt' else signal.SIGABRT, 'immediate': 'QUIT',
} }
@@ -105,7 +105,7 @@ class PostmasterProcess(psutil.Process):
except psutil.NoSuchProcess: except psutil.NoSuchProcess:
return None return None
def signal_stop(self, mode): def signal_stop(self, mode, pg_ctl='pg_ctl'):
"""Signal postmaster process to stop """Signal postmaster process to stop
:returns None if signaled, True if process is already gone, False if error :returns None if signaled, True if process is already gone, False if error
@@ -113,8 +113,10 @@ class PostmasterProcess(psutil.Process):
if self.is_single_user: if self.is_single_user:
logger.warning("Cannot stop server; single-user server is running (PID: {0})".format(self.pid)) logger.warning("Cannot stop server; single-user server is running (PID: {0})".format(self.pid))
return False return False
if os.name != 'posix':
return self.pg_ctl_kill(mode, pg_ctl)
try: try:
self.send_signal(STOP_SIGNALS[mode]) self.send_signal(getattr(signal, 'SIG' + STOP_SIGNALS[mode]))
except psutil.NoSuchProcess: except psutil.NoSuchProcess:
return True return True
except psutil.AccessDenied as e: except psutil.AccessDenied as e:
@@ -123,6 +125,16 @@ class PostmasterProcess(psutil.Process):
return None return None
def pg_ctl_kill(self, mode, pg_ctl):
try:
status = subprocess.call([pg_ctl, "kill", STOP_SIGNALS[mode], str(self.pid)])
except OSError:
return False
if status == 0:
return None
else:
return not self.is_running()
def wait_for_user_backends_to_close(self): def wait_for_user_backends_to_close(self):
# These regexps are cross checked against versions PostgreSQL 9.1 .. 11 # These regexps are cross checked against versions PostgreSQL 9.1 .. 11
aux_proc_re = re.compile("(?:postgres:)( .*:)? (?:(?:archiver|startup|autovacuum launcher|autovacuum worker|" aux_proc_re = re.compile("(?:postgres:)( .*:)? (?:(?:archiver|startup|autovacuum launcher|autovacuum worker|"
+3 -7
View File
@@ -132,13 +132,9 @@ class Rewind(object):
return leader and leader.conn_url and self._state == REWIND_STATUS.NEED return leader and leader.conn_url and self._state == REWIND_STATUS.NEED
def check_for_checkpoint_after_promote(self): def check_for_checkpoint_after_promote(self):
if self._state == REWIND_STATUS.INITIAL and self._postgresql.is_leader(): if self._state == REWIND_STATUS.INITIAL and self._postgresql.is_leader() and \
try: self._postgresql.get_master_timeline() == self._postgresql.pg_control_timeline():
timeline = int(self._postgresql.controldata().get("Latest checkpoint's TimeLineID")) self._state = REWIND_STATUS.CHECKPOINT
if self._postgresql.get_master_timeline() == timeline:
self._state = REWIND_STATUS.CHECKPOINT
except (TypeError, ValueError):
logger.exception('Failed to parse timeline from pg_controldata output')
def checkpoint_after_promote(self): def checkpoint_after_promote(self):
return self._state == REWIND_STATUS.CHECKPOINT return self._state == REWIND_STATUS.CHECKPOINT
+26
View File
@@ -1,7 +1,9 @@
import logging import logging
import os
import platform import platform
import random import random
import re import re
import tempfile
import time import time
from dateutil import tz from dateutil import tz
@@ -416,3 +418,27 @@ def cluster_as_json(cluster):
if cluster.failover.candidate: if cluster.failover.candidate:
ret['scheduled_switchover']['to'] = cluster.failover.candidate ret['scheduled_switchover']['to'] = cluster.failover.candidate
return ret return ret
def is_subpath(d1, d2):
real_d1 = os.path.realpath(d1) + os.path.sep
real_d2 = os.path.realpath(os.path.join(real_d1, d2))
return os.path.commonprefix([real_d1, real_d2 + os.path.sep]) == real_d1
def validate_directory(d, msg="{} {}"):
if not os.path.exists(d):
try:
os.makedirs(d)
except OSError as e:
logger.error(e)
raise PatroniException(msg.format(d, "couldn't create the directory"))
elif os.path.isdir(d):
try:
fd, tmpfile = tempfile.mkstemp(dir=d)
os.close(fd)
os.remove(tmpfile)
except OSError:
raise PatroniException(msg.format(d, "the directory is not writable"))
else:
raise PatroniException(msg.format(d, "is not a directory"))
+1 -1
View File
@@ -1 +1 @@
__version__ = '1.6.3' __version__ = '1.6.4'
+3 -2
View File
@@ -86,7 +86,7 @@ class MockCursor(object):
elif sql.startswith('SELECT slot_name'): elif sql.startswith('SELECT slot_name'):
self.results = [('blabla', 'physical'), ('foobar', 'physical'), ('ls', 'logical', 'a', 'b')] self.results = [('blabla', 'physical'), ('foobar', 'physical'), ('ls', 'logical', 'a', 'b')]
elif sql.startswith('SELECT CASE WHEN pg_catalog.pg_is_in_recovery()'): elif sql.startswith('SELECT CASE WHEN pg_catalog.pg_is_in_recovery()'):
self.results = [(1, 2)] self.results = [(1, 2, 1)]
elif sql.startswith('SELECT pg_catalog.pg_is_in_recovery()'): elif sql.startswith('SELECT pg_catalog.pg_is_in_recovery()'):
self.results = [(False, 2)] self.results = [(False, 2)]
elif sql.startswith('SELECT pg_catalog.to_char'): elif sql.startswith('SELECT pg_catalog.to_char'):
@@ -164,7 +164,8 @@ class PostgresInit(unittest.TestCase):
'search_path': 'public', 'hot_standby': 'on', 'max_wal_senders': 5, 'search_path': 'public', 'hot_standby': 'on', 'max_wal_senders': 5,
'wal_keep_segments': 8, 'wal_log_hints': 'on', 'max_locks_per_transaction': 64, 'wal_keep_segments': 8, 'wal_log_hints': 'on', 'max_locks_per_transaction': 64,
'max_worker_processes': 8, 'max_connections': 100, 'max_prepared_transactions': 0, 'max_worker_processes': 8, 'max_connections': 100, 'max_prepared_transactions': 0,
'track_commit_timestamp': 'off', 'unix_socket_directories': '/tmp', 'trigger_file': 'bla'} 'track_commit_timestamp': 'off', 'unix_socket_directories': '/tmp', 'trigger_file': 'bla',
'stats_temp_directory': '/tmp'}
@patch('psycopg2.connect', psycopg2_connect) @patch('psycopg2.connect', psycopg2_connect)
@patch.object(ConfigHandler, 'write_postgresql_conf', Mock()) @patch.object(ConfigHandler, 'write_postgresql_conf', Mock())
+11
View File
@@ -590,3 +590,14 @@ class TestCtl(unittest.TestCase):
mock_get_dcs.return_value.get_cluster = get_cluster_not_initialized_without_leader mock_get_dcs.return_value.get_cluster = get_cluster_not_initialized_without_leader
result = self.runner.invoke(ctl, ['reinit', 'dummy']) result = self.runner.invoke(ctl, ['reinit', 'dummy'])
assert "cluster doesn\'t have any members" in result.output assert "cluster doesn\'t have any members" in result.output
@patch('time.sleep', Mock())
@patch('patroni.ctl.get_dcs')
def test_reinit_wait(self, mock_get_dcs):
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
with patch.object(PoolManager, 'request') as mocked:
mocked.side_effect = [Mock(data=s, status=200) for s in
[b"reinitialize", b'{"state":"creating replica"}', b'{"state":"running"}']]
result = self.runner.invoke(ctl, ['reinit', 'alpha', 'other', '--wait'], input='y\ny')
self.assertIn("Waiting for reinitialize to complete on: other", result.output)
self.assertIn("Reinitialize is completed on: other", result.output)
+6 -3
View File
@@ -150,7 +150,7 @@ def run_async(self, func, args=()):
@patch.object(Postgresql, 'is_running', Mock(return_value=MockPostmaster())) @patch.object(Postgresql, 'is_running', Mock(return_value=MockPostmaster()))
@patch.object(Postgresql, 'is_leader', Mock(return_value=True)) @patch.object(Postgresql, 'is_leader', Mock(return_value=True))
@patch.object(Postgresql, 'timeline_wal_position', Mock(return_value=(1, 10))) @patch.object(Postgresql, 'timeline_wal_position', Mock(return_value=(1, 10, 1)))
@patch.object(Postgresql, '_cluster_info_state_get', Mock(return_value=3)) @patch.object(Postgresql, '_cluster_info_state_get', Mock(return_value=3))
@patch.object(Postgresql, 'call_nowait', Mock(return_value=True)) @patch.object(Postgresql, 'call_nowait', Mock(return_value=True))
@patch.object(Postgresql, 'data_directory_empty', Mock(return_value=False)) @patch.object(Postgresql, 'data_directory_empty', Mock(return_value=False))
@@ -199,9 +199,12 @@ class TestHa(PostgresInit):
self.assertTrue(self.ha.update_lock(True)) self.assertTrue(self.ha.update_lock(True))
def test_touch_member(self): def test_touch_member(self):
self.p.timeline_wal_position = Mock(return_value=(0, 1)) self.p.timeline_wal_position = Mock(return_value=(0, 1, 0))
self.p.replica_cached_timeline = Mock(side_effect=Exception) self.p.replica_cached_timeline = Mock(side_effect=Exception)
self.ha.touch_member() self.ha.touch_member()
self.p.timeline_wal_position = Mock(return_value=(0, 1, 1))
self.p.set_role('standby_leader')
self.ha.touch_member()
def test_is_leader(self): def test_is_leader(self):
self.assertFalse(self.ha.is_leader()) self.assertFalse(self.ha.is_leader())
@@ -601,7 +604,7 @@ class TestHa(PostgresInit):
# in synchronous_mode consider itself healthy if the former leader is accessible in read-only and ahead of us # in synchronous_mode consider itself healthy if the former leader is accessible in read-only and ahead of us
with patch.object(Ha, 'is_synchronous_mode', Mock(return_value=True)): with patch.object(Ha, 'is_synchronous_mode', Mock(return_value=True)):
self.assertTrue(self.ha._is_healthiest_node(self.ha.old_cluster.members)) self.assertTrue(self.ha._is_healthiest_node(self.ha.old_cluster.members))
with patch('patroni.postgresql.Postgresql.timeline_wal_position', return_value=(1, 1)): with patch('patroni.postgresql.Postgresql.timeline_wal_position', return_value=(1, 1, 1)):
self.assertFalse(self.ha._is_healthiest_node(self.ha.old_cluster.members)) self.assertFalse(self.ha._is_healthiest_node(self.ha.old_cluster.members))
with patch('patroni.postgresql.Postgresql.replica_cached_timeline', return_value=1): with patch('patroni.postgresql.Postgresql.replica_cached_timeline', return_value=1):
self.assertFalse(self.ha._is_healthiest_node(self.ha.old_cluster.members)) self.assertFalse(self.ha._is_healthiest_node(self.ha.old_cluster.members))
+8 -6
View File
@@ -46,7 +46,8 @@ class TestKubernetes(unittest.TestCase):
@patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True) @patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True)
@patch.object(Thread, 'start', Mock()) @patch.object(Thread, 'start', Mock())
def setUp(self): def setUp(self):
self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, 'labels': {'f': 'b'}}) self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0',
'loop_wait': 10, 'retry_timeout': 10, 'labels': {'f': 'b'}})
self.assertRaises(AttributeError, self.k._pods._build_cache) self.assertRaises(AttributeError, self.k._pods._build_cache)
self.k._pods._is_ready = True self.k._pods._is_ready = True
self.assertRaises(AttributeError, self.k._kinds._build_cache) self.assertRaises(AttributeError, self.k._kinds._build_cache)
@@ -71,14 +72,14 @@ class TestKubernetes(unittest.TestCase):
@patch('kubernetes.config.load_kube_config', Mock()) @patch('kubernetes.config.load_kube_config', Mock())
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock()) @patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock())
def test_update_leader(self): def test_update_leader(self):
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'}) 'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
self.assertIsNotNone(k.update_leader('123')) self.assertIsNotNone(k.update_leader('123'))
@patch('kubernetes.config.load_kube_config', Mock()) @patch('kubernetes.config.load_kube_config', Mock())
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock()) @patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock())
def test_update_leader_with_restricted_access(self): def test_update_leader_with_restricted_access(self):
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'}) 'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
self.assertIsNotNone(k.update_leader('123', True)) self.assertIsNotNone(k.update_leader('123', True))
@@ -120,7 +121,7 @@ class TestKubernetes(unittest.TestCase):
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', @patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints',
Mock(side_effect=[k8s_client.rest.ApiException(502, ''), k8s_client.rest.ApiException(500, '')])) Mock(side_effect=[k8s_client.rest.ApiException(502, ''), k8s_client.rest.ApiException(500, '')]))
def test_delete_sync_state(self): def test_delete_sync_state(self):
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'}) 'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
self.assertFalse(k.delete_sync_state()) self.assertFalse(k.delete_sync_state())
@@ -139,7 +140,7 @@ class TestKubernetes(unittest.TestCase):
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_service', @patch.object(k8s_client.CoreV1Api, 'create_namespaced_service',
Mock(side_effect=[True, False, k8s_client.rest.ApiException(500, '')])) Mock(side_effect=[True, False, k8s_client.rest.ApiException(500, '')]))
def test__create_config_service(self): def test__create_config_service(self):
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'}) 'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'})) self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'}))
self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'})) self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'}))
@@ -152,7 +153,8 @@ class TestCacheBuilder(unittest.TestCase):
@patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True) @patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True)
@patch.object(Thread, 'start', Mock()) @patch.object(Thread, 'start', Mock())
def setUp(self): def setUp(self):
self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, 'labels': {'f': 'b'}}) self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0',
'loop_wait': 10, 'retry_timeout': 10, 'labels': {'f': 'b'}})
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map) @patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map)
@patch('patroni.dcs.kubernetes.ObjectCache._watch') @patch('patroni.dcs.kubernetes.ObjectCache._watch')
+1
View File
@@ -174,6 +174,7 @@ class TestPatroni(unittest.TestCase):
@patch.object(Thread, 'join', Mock()) @patch.object(Thread, 'join', Mock())
def test_shutdown(self): def test_shutdown(self):
self.p.api.shutdown = Mock(side_effect=Exception) self.p.api.shutdown = Mock(side_effect=Exception)
self.p.ha.shutdown = Mock(side_effect=Exception)
self.p.shutdown() self.p.shutdown()
def test_check_psycopg2(self): def test_check_psycopg2(self):
+13 -5
View File
@@ -8,7 +8,7 @@ import time
from mock import Mock, MagicMock, PropertyMock, patch, mock_open from mock import Mock, MagicMock, PropertyMock, patch, mock_open
from patroni.async_executor import CriticalTask from patroni.async_executor import CriticalTask
from patroni.dcs import Cluster, ClusterConfig, Member, RemoteMember, SyncState from patroni.dcs import Cluster, ClusterConfig, Member, RemoteMember, SyncState
from patroni.exceptions import PostgresConnectionException from patroni.exceptions import PostgresConnectionException, PatroniException
from patroni.postgresql import Postgresql, STATE_REJECT, STATE_NO_RESPONSE from patroni.postgresql import Postgresql, STATE_REJECT, STATE_NO_RESPONSE
from patroni.postgresql.postmaster import PostmasterProcess from patroni.postgresql.postmaster import PostmasterProcess
from patroni.postgresql.slots import SlotsHandler from patroni.postgresql.slots import SlotsHandler
@@ -206,14 +206,16 @@ class TestPostgresql(BaseTestPostgresql):
@patch('patroni.postgresql.config.ConfigHandler._get_pg_settings') @patch('patroni.postgresql.config.ConfigHandler._get_pg_settings')
def test_check_recovery_conf(self, mock_get_pg_settings): def test_check_recovery_conf(self, mock_get_pg_settings):
mock_get_pg_settings.return_value = { mock_get_pg_settings.return_value = {
'primary_conninfo': ['primary_conninfo', 'foo=', None, 'string', 'postmaster'], 'primary_conninfo': ['primary_conninfo', 'foo=', None, 'string', 'postmaster', self.p.config._auto_conf],
'recovery_min_apply_delay': ['recovery_min_apply_delay', '0', 'ms', 'integer', 'sighup'] 'recovery_min_apply_delay': ['recovery_min_apply_delay', '0', 'ms', 'integer', 'sighup', 'foo']
} }
self.assertEqual(self.p.config.check_recovery_conf(None), (True, True)) self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
self.p.config.write_recovery_conf({'standby_mode': 'on'}) self.p.config.write_recovery_conf({'standby_mode': 'on'})
self.assertEqual(self.p.config.check_recovery_conf(None), (True, True)) self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
mock_get_pg_settings.return_value['primary_conninfo'][1] = '' mock_get_pg_settings.return_value['primary_conninfo'][1] = ''
mock_get_pg_settings.return_value['recovery_min_apply_delay'][1] = '1' mock_get_pg_settings.return_value['recovery_min_apply_delay'][1] = '1'
self.assertEqual(self.p.config.check_recovery_conf(None), (False, False))
mock_get_pg_settings.return_value['recovery_min_apply_delay'][5] = self.p.config._auto_conf
self.assertEqual(self.p.config.check_recovery_conf(None), (True, False)) self.assertEqual(self.p.config.check_recovery_conf(None), (True, False))
mock_get_pg_settings.return_value['recovery_min_apply_delay'][1] = '0' mock_get_pg_settings.return_value['recovery_min_apply_delay'][1] = '0'
self.assertEqual(self.p.config.check_recovery_conf(None), (False, False)) self.assertEqual(self.p.config.check_recovery_conf(None), (False, False))
@@ -234,7 +236,8 @@ class TestPostgresql(BaseTestPostgresql):
@patch.object(MockPostmaster, 'create_time', Mock(return_value=1234567), create=True) @patch.object(MockPostmaster, 'create_time', Mock(return_value=1234567), create=True)
@patch('patroni.postgresql.config.ConfigHandler._get_pg_settings') @patch('patroni.postgresql.config.ConfigHandler._get_pg_settings')
def test__read_recovery_params(self, mock_get_pg_settings): def test__read_recovery_params(self, mock_get_pg_settings):
mock_get_pg_settings.return_value = {'primary_conninfo': ['primary_conninfo', '', None, 'string', 'postmaster']} mock_get_pg_settings.return_value = {'primary_conninfo': ['primary_conninfo', '', None, 'string',
'postmaster', self.p.config._postgresql_conf]}
self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': {'password': 'foo'}}) self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': {'password': 'foo'}})
self.p.config.write_postgresql_conf() self.p.config.write_postgresql_conf()
self.assertEqual(self.p.config.check_recovery_conf(None), (False, False)) self.assertEqual(self.p.config.check_recovery_conf(None), (False, False))
@@ -331,7 +334,7 @@ class TestPostgresql(BaseTestPostgresql):
self.assertTrue(self.p.promote(0)) self.assertTrue(self.p.promote(0))
def test_timeline_wal_position(self): def test_timeline_wal_position(self):
self.assertEqual(self.p.timeline_wal_position(), (1, 2)) self.assertEqual(self.p.timeline_wal_position(), (1, 2, 1))
Thread(target=self.p.timeline_wal_position).start() Thread(target=self.p.timeline_wal_position).start()
@patch.object(PostmasterProcess, 'from_pidfile') @patch.object(PostmasterProcess, 'from_pidfile')
@@ -695,3 +698,8 @@ class TestPostgresql(BaseTestPostgresql):
self.p.cancellable.cancel() self.p.cancellable.cancel()
self.assertFalse(self.p.start()) self.assertFalse(self.p.start())
self.assertTrue(self.p.pending_restart) self.assertTrue(self.p.pending_restart)
@patch('os.path.exists', Mock(return_value=True))
@patch('os.path.isfile', Mock(return_value=False))
def test_pgpass_is_dir(self):
self.assertRaises(PatroniException, self.setUp)
+17
View File
@@ -66,6 +66,8 @@ class TestPostmasterProcess(unittest.TestCase):
@patch('psutil.Process.__init__', Mock()) @patch('psutil.Process.__init__', Mock())
@patch('psutil.Process.send_signal') @patch('psutil.Process.send_signal')
@patch('psutil.Process.pid', Mock(return_value=123)) @patch('psutil.Process.pid', Mock(return_value=123))
@patch('os.name', 'posix')
@patch('signal.SIGQUIT', 3, create=True)
def test_signal_stop(self, mock_send_signal): def test_signal_stop(self, mock_send_signal):
proc = PostmasterProcess(-123) proc = PostmasterProcess(-123)
self.assertEqual(proc.signal_stop('immediate'), False) self.assertEqual(proc.signal_stop('immediate'), False)
@@ -76,6 +78,21 @@ class TestPostmasterProcess(unittest.TestCase):
self.assertEqual(proc.signal_stop('immediate'), True) self.assertEqual(proc.signal_stop('immediate'), True)
self.assertEqual(proc.signal_stop('immediate'), False) self.assertEqual(proc.signal_stop('immediate'), False)
@patch('psutil.Process.__init__', Mock())
@patch('patroni.postgresql.postmaster.os')
@patch('subprocess.call', Mock(side_effect=[0, OSError, 1]))
@patch('psutil.Process.pid', Mock(return_value=123))
@patch('psutil.Process.is_running', Mock(return_value=False))
def test_signal_stop_nt(self, mock_os):
mock_os.configure_mock(name="nt")
proc = PostmasterProcess(-123)
self.assertEqual(proc.signal_stop('immediate'), False)
proc = PostmasterProcess(123)
self.assertEqual(proc.signal_stop('immediate'), None)
self.assertEqual(proc.signal_stop('immediate'), False)
self.assertEqual(proc.signal_stop('immediate'), True)
@patch('psutil.Process.__init__', Mock()) @patch('psutil.Process.__init__', Mock())
@patch('psutil.wait_procs') @patch('psutil.wait_procs')
def test_wait_for_user_backends_to_close(self, mock_wait): def test_wait_for_user_backends_to_close(self, mock_wait):
+24 -1
View File
@@ -2,7 +2,7 @@ import unittest
from mock import Mock, patch from mock import Mock, patch
from patroni.exceptions import PatroniException from patroni.exceptions import PatroniException
from patroni.utils import Retry, RetryFailedError, polling_loop from patroni.utils import Retry, RetryFailedError, polling_loop, validate_directory
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
@@ -10,6 +10,29 @@ class TestUtils(unittest.TestCase):
def test_polling_loop(self): def test_polling_loop(self):
self.assertEqual(list(polling_loop(0.001, interval=0.001)), [0]) self.assertEqual(list(polling_loop(0.001, interval=0.001)), [0])
@patch('os.path.exists', Mock(return_value=True))
@patch('os.path.isdir', Mock(return_value=True))
@patch('tempfile.mkstemp', Mock(return_value=("", "")))
@patch('os.remove', Mock(side_effect=Exception))
def test_validate_directory_writable(self):
self.assertRaises(Exception, validate_directory, "/tmp")
@patch('os.path.exists', Mock(return_value=True))
@patch('os.path.isdir', Mock(return_value=True))
@patch('tempfile.mkstemp', Mock(side_effect=OSError))
def test_validate_directory_not_writable(self):
self.assertRaises(PatroniException, validate_directory, "/tmp")
@patch('os.path.exists', Mock(return_value=False))
@patch('os.makedirs', Mock(side_effect=OSError))
def test_validate_directory_couldnt_create(self):
self.assertRaises(PatroniException, validate_directory, "/tmp")
@patch('os.path.exists', Mock(return_value=True))
@patch('os.path.isdir', Mock(return_value=False))
def test_validate_directory_is_not_a_directory(self):
self.assertRaises(PatroniException, validate_directory, "/tmp")
@patch('time.sleep', Mock()) @patch('time.sleep', Mock())
class TestRetrySleeper(unittest.TestCase): class TestRetrySleeper(unittest.TestCase):