mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-26 23:50:23 +00:00
Compare commits
+9
-12
@@ -1,45 +1,42 @@
|
||||
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
|
||||
## It has all the necessary components to play/debug with a single node appliance, running etcd
|
||||
FROM postgres:9.6
|
||||
FROM postgres:10
|
||||
MAINTAINER Alexander Kukushkin <[email protected]>
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive \
|
||||
&& echo 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";' > /etc/apt/apt.conf.d/01norecommend \
|
||||
&& apt-get update -y \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y curl jq haproxy python-psycopg2 python-yaml python-requests python-six python-pysocks \
|
||||
python-dateutil python-pip python-setuptools python-prettytable python-wheel python-psutil python locales \
|
||||
# postgres:10 is based on debian, which has patroni package. We will install all required dependencies
|
||||
&& apt-get install -s patroni | sed -n -e '/^Inst patroni /d' -e 's/^Inst \([^ ]\+\) .*$/\1/p' \
|
||||
| xargs apt-get install -y curl jq haproxy locales python3-etcd python3-kazoo \
|
||||
|
||||
## Make sure we have a en_US.UTF-8 locale available
|
||||
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
|
||||
|
||||
&& pip install 'python-etcd>=0.4.3,<0.5' click tzlocal cdiff \
|
||||
|
||||
&& mkdir -p /home/postgres \
|
||||
&& chown postgres:postgres /home/postgres \
|
||||
|
||||
# Clean up
|
||||
&& apt-get remove -y python-pip python-setuptools \
|
||||
&& apt-get purge -y libpython2.7-stdlib libpython2.7-minimal \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/* /root/.cache
|
||||
|
||||
ENV ETCDVERSION 3.2.3
|
||||
ENV ETCDVERSION 3.2.23
|
||||
RUN curl -L https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-amd64.tar.gz \
|
||||
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl
|
||||
|
||||
ENV CONFDVERSION 0.11.0
|
||||
ENV CONFDVERSION 0.16.0
|
||||
RUN curl -L https://github.com/kelseyhightower/confd/releases/download/v${CONFDVERSION}/confd-${CONFDVERSION}-linux-amd64 > /usr/local/bin/confd \
|
||||
&& chmod +x /usr/local/bin/confd
|
||||
|
||||
ADD patronictl.py patroni.py docker/entrypoint.sh /
|
||||
ADD patroni /patroni/
|
||||
ADD extras/confd /etc/confd
|
||||
RUN ln -s /patronictl.py /usr/local/bin/patronictl
|
||||
|
||||
### Setting up a simple script that will serve as an entrypoint
|
||||
RUN mkdir /data/ && touch /pgpass /patroni.yml \
|
||||
&& chown postgres:postgres -R /patroni/ /data/ /pgpass /patroni.yml /etc/haproxy /var/run/ /var/lib/ /var/log/
|
||||
RUN sed -i 's/env python/&3/' patroni*.py && ln -s /patronictl.py /usr/local/bin/patronictl && mkdir /data/ /run/haproxy \
|
||||
&& touch /pgpass /patroni.yml && chown postgres:postgres -R /patroni/ /data/ /pgpass /patroni.yml /etc/haproxy /var/run/ /var/lib/ /var/log/
|
||||
|
||||
EXPOSE 2379 5432 8008
|
||||
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ dbnode1:
|
||||
- ./patroni:/patroni
|
||||
env_file: docker/patroni-secrets.env
|
||||
environment:
|
||||
PATRONI_ETCD_HOST: patroni_etcd:2379
|
||||
PATRONI_ETCD_URL: http://patroni_etcd:2379
|
||||
PATRONI_NAME: dbnode1
|
||||
PATRONI_SCOPE: testcluster
|
||||
|
||||
@@ -28,7 +28,7 @@ dbnode2:
|
||||
- ./patroni:/patroni
|
||||
env_file: docker/patroni-secrets.env
|
||||
environment:
|
||||
PATRONI_ETCD_HOST: patroni_etcd:2379
|
||||
PATRONI_ETCD_URL: http://patroni_etcd:2379
|
||||
PATRONI_NAME: dbnode2
|
||||
PATRONI_SCOPE: testcluster
|
||||
|
||||
@@ -41,7 +41,7 @@ dbnode3:
|
||||
- ./patroni:/patroni
|
||||
env_file: docker/patroni-secrets.env
|
||||
environment:
|
||||
PATRONI_ETCD_HOST: patroni_etcd:2379
|
||||
PATRONI_ETCD_URL: http://patroni_etcd:2379
|
||||
PATRONI_NAME: dbnode3
|
||||
PATRONI_SCOPE: testcluster
|
||||
|
||||
@@ -53,6 +53,6 @@ haproxy:
|
||||
- "5000:5000"
|
||||
- "5001:5001"
|
||||
environment:
|
||||
PATRONI_ETCD_HOST: patroni_etcd:2379
|
||||
PATRONI_ETCD_URL: http://patroni_etcd:2379
|
||||
PATRONI_SCOPE: testcluster
|
||||
command: --confd
|
||||
|
||||
@@ -84,7 +84,7 @@ function docker_run()
|
||||
ETCD_CONTAINER="${PATRONI_SCOPE}_etcd"
|
||||
docker_run ${ETCD_CONTAINER} ${DOCKER_IMAGE} --etcd
|
||||
|
||||
DOCKER_ARGS="--link=${ETCD_CONTAINER}:${ETCD_CONTAINER} -e PATRONI_SCOPE=${PATRONI_SCOPE} -e PATRONI_ETCD_HOST=${ETCD_CONTAINER}:2379"
|
||||
DOCKER_ARGS="--link=${ETCD_CONTAINER}:${ETCD_CONTAINER} -e PATRONI_SCOPE=${PATRONI_SCOPE} -e PATRONI_ETCD_URL=http://${ETCD_CONTAINER}:2379"
|
||||
PATRONI_ENV=$(sed 's/#.*//g' docker/patroni-secrets.env | sed -n 's/^PATRONI_.*$/-e &/p' | tr '\n' ' ')
|
||||
PATRONI_VOLUME="-v $(dirname $(dirname $(realpath $0)))/patroni:/patroni"
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ while getopts "$optspec" optchar; do
|
||||
done
|
||||
exec $CONFD zookeeper -node ${PATRONI_ZOOKEEPER_HOSTS}
|
||||
else
|
||||
while ! curl -s ${PATRONI_ETCD_HOST}/v2/members | jq -r '.members[0].clientURLs[0]' | grep -q http; do
|
||||
while ! curl -s ${PATRONI_ETCD_URL}/v2/members | jq -r '.members[0].clientURLs[0]' | grep -q http; do
|
||||
sleep 1
|
||||
done
|
||||
exec $CONFD etcd -node $PATRONI_ETCD_HOST
|
||||
exec $CONFD etcd -node $PATRONI_ETCD_URL
|
||||
fi
|
||||
;;
|
||||
etcd)
|
||||
@@ -74,9 +74,9 @@ while getopts "$optspec" optchar; do
|
||||
done
|
||||
|
||||
## We start an etcd
|
||||
if [[ -z ${PATRONI_ETCD_HOST} && -z ${PATRONI_ZOOKEEPER_HOSTS} ]]; then
|
||||
if [[ -z ${PATRONI_ETCD_URL} && -z ${PATRONI_ZOOKEEPER_HOSTS} ]]; then
|
||||
etcd $ETCD_ARGS > /var/log/etcd.log 2> /var/log/etcd.err &
|
||||
export PATRONI_ETCD_HOST="127.0.0.1:2379"
|
||||
export PATRONI_ETCD_URL="http://127.0.0.1:2379"
|
||||
fi
|
||||
|
||||
export PATRONI_SCOPE
|
||||
@@ -108,7 +108,7 @@ __EOF__
|
||||
mkdir -p "$HOME/.config/patroni"
|
||||
[ -h "$HOME/.config/patroni/patronictl.yaml" ] || ln -s /patroni.yml "$HOME/.config/patroni/patronictl.yaml"
|
||||
|
||||
[ -z $CHEAT ] && exec python /patroni.py /patroni.yml
|
||||
[ -z $CHEAT ] && exec python3 /patroni.py /patroni.yml
|
||||
|
||||
while true; do
|
||||
sleep 60
|
||||
|
||||
+5
-4
@@ -19,6 +19,7 @@ Bootstrap configuration
|
||||
- **maximum\_lag\_on\_failover**: the maximum bytes a follower may lag to be able to participate in leader election.
|
||||
- **master\_start\_timeout**: the amount of time a master is allowed to recover from failures before failover is triggered. Default is 300 seconds. When set to 0 failover is done immediately after a crash is detected if possible. When using asynchronous replication a failover can cause lost transactions. Best worst case failover time for master failure is: loop\_wait + master\_start\_timeout + loop\_wait, unless master\_start\_timeout is zero, in which case it's just loop\_wait. Set the value according to your durability/availability tradeoff.
|
||||
- **synchronous\_mode**: turns on synchronous replication mode. In this mode a replica will be chosen as synchronous and only the latest leader and synchronous replica are able to participate in leader election. Synchronous mode makes sure that successfully committed transactions will not be lost at failover, at the cost of losing availability for writes when Patroni cannot ensure transaction durability. See :ref:`replication modes documentation <replication_modes>` for details.
|
||||
- **synchronous\_mode\_strict**: prevents disabling synchronous replication if no synchronous replicas are available, blocking all client writes to the master. See :ref:`replication modes documentation <replication_modes>` for details.
|
||||
- **postgresql**:
|
||||
- **use\_pg\_rewind**: whether or not to use pg_rewind
|
||||
- **use\_slots**: whether or not to use replication_slots. Must be False for PostgreSQL 9.3. You should comment out max_replication_slots before it becomes ineligible for leader status.
|
||||
@@ -113,7 +114,7 @@ PostgreSQL
|
||||
- **on\_start**: run this script when the cluster starts.
|
||||
- **on\_stop**: run this script when the cluster stops.
|
||||
- **connect\_address**: IP address + port through which Postgres is accessible from other nodes and applications.
|
||||
- **create\_replica\_method**: an ordered list of the create methods for turning a Patroni node into a new replica.
|
||||
- **create\_replica\_methods**: an ordered list of the create methods for turning a Patroni node into a new replica.
|
||||
"basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its
|
||||
own config item. See :ref:`custom replica creation methods documentation <custom_replica_creation>` for further explanation.
|
||||
- **data\_dir**: The location of the Postgres data directory, either existing or to be initialized by Patroni.
|
||||
@@ -131,12 +132,12 @@ PostgreSQL
|
||||
- **pg\_ctl\_timeout**: How long should pg_ctl wait when doing ``start``, ``stop`` or ``restart``. Default value is 60 seconds.
|
||||
- **use\_pg\_rewind**: try to use pg\_rewind on the former leader when it joins cluster as a replica.
|
||||
- **remove\_data\_directory\_on\_rewind\_failure**: If this option is enabled, Patroni will remove postgres data directory and recreate replica. Otherwise it will try to follow the new leader. Default value is **false**.
|
||||
- **replica\_method**: for each create_replica_method other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value".
|
||||
- **replica\_method**: for each create_replica_methods other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value".
|
||||
|
||||
REST API
|
||||
--------
|
||||
- **connect\_address**: IP address and port to access the REST API.
|
||||
- **listen**: IP address and port that Patroni will listen to, to provide health-check information for HAProxy.
|
||||
- **connect\_address**: IP address (or hostname) and port, to access the Patroni's REST API. It can serve as a endpoint for HTTP health checks (read below about the "listen" REST API parameter), and also for user queries (either directly or via the REST API), as well as for the health checks done by the cluster members during leader elections (for example, to determine whether the master is still running, or if there is a node which has a WAL position that is ahead of the one doing the query; etc.) The connect_address is put in the member key in DCS, making it possible to translate the member name into the address to connect to its REST API.
|
||||
- **listen**: IP address (or hostname) and port that Patroni will listen to for the REST API - to provide also the same health checks and cluster messaging between the participating nodes, as described above. to provide health-check information for HAProxy (or any other load balancer capable of doing a HTTP "OPTION" or "GET" checks)
|
||||
- **Optional**:
|
||||
- **authentication**:
|
||||
- **username**: Basic-auth username to protect unsafe REST API endpoints.
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
Release notes
|
||||
=============
|
||||
|
||||
Version 1.4.5
|
||||
-------------
|
||||
|
||||
**New features**
|
||||
|
||||
- Improve logging when applying new postgres configuration (Don Seiler)
|
||||
|
||||
Patroni logs changed parameter names and values.
|
||||
|
||||
- Python 3.7 compatibility (Christoph Berg)
|
||||
|
||||
async is a reserved keyword in python3.7
|
||||
|
||||
- Set state to "stopped" in the DCS when a member is shut down (Tony Sorrentino)
|
||||
|
||||
This shows the member state as "stopped" in "patronictl list" command.
|
||||
|
||||
- Improve the message logged when stale postmaster.pid matches a running process (Ants Aasma)
|
||||
|
||||
The previous one was beyond confusing.
|
||||
|
||||
- Implement patronictl reload functionality (Don Seiler)
|
||||
|
||||
Before that it was only possible to reload configuration by either calling REST API or by sending SIGHUP signal to the Patroni process.
|
||||
|
||||
- Take and apply some parameters from controldata when starting as a replica (Alexander Kukushkin)
|
||||
|
||||
The value of `max_connections` and some other parameters set in the global configuration may be lower than the one actually used by the primary; when this happens, the replica cannot start and should be fixed manually. Patroni takes care of that now by reading and applying the value from `pg_controldata`, starting postgres and setting `pending_restart` flag.
|
||||
|
||||
- If set, use LD_LIBRARY_PATH when starting postgres (Chris Fraser)
|
||||
|
||||
When starting up Postgres, Patroni was passing along PATH, LC_ALL and LANG env vars if they are set. Now it is doing the same with LD_LIBRARY_PATH. It should help if somebody installed PostgreSQL to non-standard place.
|
||||
|
||||
- Rename create_replica_method to create_replica_methods (Dmitry Dolgov)
|
||||
|
||||
To make it clear that it's actually an array. The old name is still supported for backward compatibility.
|
||||
|
||||
**Bug fixes and stability improvements**
|
||||
|
||||
- Fix condition for the replica start due to pg_rewind in paused state (Oleksii Kliukin)
|
||||
|
||||
Avoid starting the replica that had already executed pg_rewind before.
|
||||
|
||||
- Respond 200 to the master health-check only if update_lock has been successful (Alexander)
|
||||
|
||||
Prevent Patroni from reporting itself a master on the former (demoted) master if DCS is partitioned.
|
||||
|
||||
- Fix compatibility with the new consul module (Alexander)
|
||||
|
||||
Starting from v1.1.0 python-consul changed internal API and started using `list` instead of `dict` to pass query parameters.
|
||||
|
||||
- Catch exceptions from Patroni REST API thread during shutdown (Alexander)
|
||||
|
||||
Those uncaught exceptions kept PostgreSQL running at shutdown.
|
||||
|
||||
- Do crash recovery only when Postgres runs as the master (Alexander)
|
||||
|
||||
Require `pg_controldata` to report 'in production' or 'shutting down' or 'in crash recovery'. In all other cases no crash recovery is necessary.
|
||||
|
||||
- Improve handling of configuration errors (Henning Jacobs, Alexander)
|
||||
|
||||
It is possible to change a lot of parameters in runtime (including `restapi.listen`) by updating Patroni config file and sending SIGHUP to Patroni process. This fix eliminates obscure exceptions from the 'restapi' thread when some of the parameters receive invalid values.
|
||||
|
||||
|
||||
Version 1.4.4
|
||||
-------------
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ scripts to clone a new replica. Those are configured in the ``postgresql`` confi
|
||||
.. code:: YAML
|
||||
|
||||
postgresql:
|
||||
create_replica_method:
|
||||
create_replica_methods:
|
||||
- wal_e
|
||||
- basebackup
|
||||
wal_e:
|
||||
@@ -80,7 +80,7 @@ scripts to clone a new replica. Those are configured in the ``postgresql`` confi
|
||||
max-rate: '100M'
|
||||
|
||||
|
||||
The ``create_replica_method`` defines available replica creation methods and the order of executing them. Patroni will
|
||||
The ``create_replica_methods`` defines available replica creation methods and the order of executing them. Patroni will
|
||||
stop on the first one that returns 0. Each method should define a separate section in the configuration file, listing the command
|
||||
to execute and any custom parameters that should be passed to that command. All parameters will be passed in a
|
||||
``--name=value`` format. Besides user-defined parameters, Patroni supplies a couple of cluster-specific ones:
|
||||
@@ -99,8 +99,9 @@ A special ``no_master`` parameter, if defined, allows Patroni to call the replic
|
||||
running master or replicas. In that case, an empty string will be passed in a connection string. This is useful for
|
||||
restoring the formerly running cluster from the binary backup.
|
||||
|
||||
A ``basebackup`` method is a special case: it will be used if ``create_replica_method`` is empty, although it is possible
|
||||
to list it explicitly among the ``create_replica_method`` methods. This method initializes a new replica with the
|
||||
A ``basebackup`` method is a special case: it will be used if
|
||||
``create_replica_methods`` is empty, although it is possible
|
||||
to list it explicitly among the ``create_replica_methods`` methods. This method initializes a new replica with the
|
||||
``pg_basebackup``, the base backup is taken from the master unless there are replicas with ``clonefrom`` tag, in which case one
|
||||
of such replicas will be used as the origin for pg_basebackup. It works without any configuration; however, it is
|
||||
possible to specify a ``basebackup`` configuration section. Same rules as with the other method configuration apply,
|
||||
|
||||
@@ -42,6 +42,15 @@ Turning on ``synchronous_mode`` does not guarantee multi node durability of comm
|
||||
|
||||
When ``synchronous_mode`` is on and a standby crashes, commits will block until next iteration of Patroni runs and switches the primary to standalone mode (worst case delay for writes ``ttl`` seconds, average case ``loop_wait``/2 seconds). Manually shutting down or restarting a standby will not cause a commit service interruption. Standby will signal the primary to release itself from synchronous standby duties before PostgreSQL shutdown is initiated.
|
||||
|
||||
When it is absolutely necessary to guarantee that each write is stored durably
|
||||
on at least two nodes, enable ``synchronous_mode_strict`` in addition to the
|
||||
``synchronous_node``. This parameter prevents Patroni from switching off the
|
||||
synchronous replication on the primary when no synchronous standby candidates
|
||||
are available. As a downside, the primary is not be available for writes
|
||||
(unless the Postgres transaction explicitly turns of ``synchronous_mode``),
|
||||
blocking all client write requests until at least one synchronous replica comes
|
||||
up.
|
||||
|
||||
You can ensure that a standby never becomes the synchronous standby by setting ``nosync`` tag to true. This is recommended to set for standbys that are behind slow network connections and would cause performance degradation when becoming a synchronous standby.
|
||||
|
||||
Synchronous mode can be switched on and off via Patroni REST interface. See :ref:`dynamic configuration <dynamic_configuration>` for instructions.
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
`confd` directory contains haproxy template files for the [confd](https://github.com/kelseyhightower/confd) -- lightweight configuration management tool
|
||||
You need to copy content of `confd` directory into /etcd/confd and run confd service:
|
||||
```bash
|
||||
$ confd -prefix=/service/$PATRONI_SCOPE -backend etcd -node $PATRONI_ETCD_HOST -interval=10
|
||||
$ confd -prefix=/service/$PATRONI_SCOPE -backend etcd -node $PATRONI_ETCD_URL -interval=10
|
||||
```
|
||||
It will periodically update haproxy.cfg with the actual list of Patroni nodes from `etcd` and "reload" haproxy when it is necessary.
|
||||
|
||||
|
||||
@@ -11,12 +11,20 @@ Type=simple
|
||||
User=postgres
|
||||
Group=postgres
|
||||
|
||||
# Read in configuration file if it exists, otherwise proceed
|
||||
EnvironmentFile=-/etc/patroni_env.conf
|
||||
|
||||
WorkingDirectory=~
|
||||
|
||||
# Where to send early-startup messages from the server
|
||||
# This is normally controlled by the global default set by systemd
|
||||
# StandardOutput=syslog
|
||||
|
||||
ExecStart=/bin/patroni /etc/patroni.yml
|
||||
|
||||
# Send HUP to reload from patroni.yml
|
||||
ExecReload=/bin/kill -s HUP $MAINPID
|
||||
|
||||
# only kill the patroni process, not it's children, so it will gracefully stop postgres
|
||||
KillMode=process
|
||||
|
||||
|
||||
@@ -612,6 +612,13 @@ class PatroniPoolController(object):
|
||||
'method': 'pg_basebackup',
|
||||
'pg_basebackup': {
|
||||
'command': self.BACKUP_SCRIPT + ' --walmethod=stream --dbname=' + f.backup_source
|
||||
},
|
||||
'dcs': {
|
||||
'postgresql': {
|
||||
'parameters': {
|
||||
'max_connections': 101
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'postgresql': {
|
||||
|
||||
@@ -145,6 +145,8 @@ rules:
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
# delete is required only for 'patronictl remove'
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
@@ -157,6 +159,8 @@ rules:
|
||||
- create
|
||||
- list
|
||||
- watch
|
||||
# delete is required only for for 'patronictl remove'
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
|
||||
+4
-1
@@ -129,7 +129,10 @@ class Patroni(object):
|
||||
signal.signal(signal.SIGTERM, self.sigterm_handler)
|
||||
|
||||
def shutdown(self):
|
||||
self.api.shutdown()
|
||||
try:
|
||||
self.api.shutdown()
|
||||
except Exception:
|
||||
logger.exception('Exception during RestApi.shutdown')
|
||||
self.ha.shutdown()
|
||||
|
||||
|
||||
|
||||
+25
-14
@@ -8,7 +8,8 @@ import dateutil.parser
|
||||
import datetime
|
||||
|
||||
from patroni.postgresql import PostgresConnectionException, PostgresException, Postgresql
|
||||
from patroni.utils import deep_compare, parse_bool, patch_config, Retry, RetryFailedError, parse_int, tzutc
|
||||
from patroni.utils import deep_compare, parse_bool, patch_config, Retry, \
|
||||
RetryFailedError, parse_int, split_host_port, tzutc
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.socketserver import ThreadingMixIn
|
||||
from threading import Thread
|
||||
@@ -91,7 +92,7 @@ class RestApiHandler(BaseHTTPRequestHandler):
|
||||
return response.get('role') == 'replica' and not patroni.noloadbalance
|
||||
|
||||
if cluster: # dcs available
|
||||
if cluster.leader and cluster.leader.name == patroni.postgresql.name: # is_leader
|
||||
if patroni.ha.is_leader():
|
||||
status_code = 200 if 'master' in path else 503
|
||||
elif 'role' not in response:
|
||||
status_code = 503
|
||||
@@ -473,6 +474,7 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
|
||||
|
||||
def __init__(self, patroni, config):
|
||||
self.patroni = patroni
|
||||
self.__listen = None
|
||||
self.__initialize(config)
|
||||
self.__set_config_parameters(config)
|
||||
self.daemon = True
|
||||
@@ -507,18 +509,25 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
|
||||
def __get_ssl_options(config):
|
||||
return {option: config[option] for option in ['certfile', 'keyfile'] if option in config}
|
||||
|
||||
def __set_connection_string(self, connect_address):
|
||||
self.connection_string = '{0}://{1}/patroni'.format(self.__protocol, connect_address or self.__listen)
|
||||
|
||||
def __set_config_parameters(self, config):
|
||||
self.__auth_key = base64.b64encode(config['auth'].encode('utf-8')).decode('utf-8') if 'auth' in config else None
|
||||
self.__set_connection_string(config.get('connect_address'))
|
||||
self.connection_string = '{0}://{1}/patroni'.format(self.__protocol,
|
||||
config.get('connect_address') or self.__listen)
|
||||
|
||||
def __initialize(self, config):
|
||||
self.__ssl_options = self.__get_ssl_options(config)
|
||||
try:
|
||||
host, port = split_host_port(config['listen'], None)
|
||||
except Exception:
|
||||
raise ValueError('Invalid "restapi" config: expected <HOST>:<PORT> for "listen", but got "{0}"'
|
||||
.format(config['listen']))
|
||||
|
||||
if self.__listen is not None: # changing config in runtime
|
||||
self.shutdown()
|
||||
|
||||
self.__listen = config['listen']
|
||||
host, port = config['listen'].rsplit(':', 1)
|
||||
HTTPServer.__init__(self, (host, int(port)), RestApiHandler)
|
||||
self.__ssl_options = self.__get_ssl_options(config)
|
||||
|
||||
HTTPServer.__init__(self, (host, port), RestApiHandler)
|
||||
Thread.__init__(self, target=self.serve_forever)
|
||||
self._set_fd_cloexec(self.socket)
|
||||
|
||||
@@ -530,11 +539,13 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread):
|
||||
import ssl
|
||||
self.socket = ssl.wrap_socket(self.socket, server_side=True, **self.__ssl_options)
|
||||
self.__protocol = 'https'
|
||||
self.__set_connection_string(config.get('connect_address'))
|
||||
return True
|
||||
|
||||
def reload_config(self, config):
|
||||
self.__set_config_parameters(config)
|
||||
if self.__listen != config['listen'] or self.__ssl_options != self.__get_ssl_options(config):
|
||||
self.shutdown()
|
||||
self.__initialize(config)
|
||||
if 'listen' not in config: # changing config in runtime
|
||||
raise ValueError('Can not find "restapi.listen" config')
|
||||
|
||||
elif (self.__listen != config['listen'] or self.__ssl_options != self.__get_ssl_options(config)) \
|
||||
and self.__initialize(config):
|
||||
self.start()
|
||||
self.__set_config_parameters(config)
|
||||
|
||||
@@ -158,6 +158,8 @@ class Config(object):
|
||||
self._local_configuration = configuration
|
||||
self.__effective_configuration = new_configuration
|
||||
return True
|
||||
else:
|
||||
logger.info('No configuration items changed, nothing to reload.')
|
||||
except Exception:
|
||||
logger.exception('Exception when reloading local configuration from %s', self.config_file)
|
||||
if dry_run:
|
||||
|
||||
@@ -445,6 +445,33 @@ def parse_scheduled(scheduled):
|
||||
return None
|
||||
|
||||
|
||||
@ctl.command('reload', help='Reload cluster member configuration')
|
||||
@click.argument('cluster_name')
|
||||
@click.argument('member_names', nargs=-1)
|
||||
@click.option('--role', '-r', help='Reload only members with this role', default='any',
|
||||
type=click.Choice(['master', 'replica', 'any']))
|
||||
@option_force
|
||||
@click.pass_obj
|
||||
def reload(obj, cluster_name, member_names, force, role):
|
||||
cluster = get_dcs(obj, cluster_name).get_cluster()
|
||||
|
||||
members = get_members(cluster, cluster_name, member_names, role, force, 'reload')
|
||||
|
||||
content = {}
|
||||
for member in members:
|
||||
r = request_patroni(member, 'post', 'reload', content, auth_header(obj))
|
||||
if r.status_code == 200:
|
||||
click.echo('No changes to apply on member {0}'.format(member.name))
|
||||
elif r.status_code == 202:
|
||||
click.echo('Reload request received for member {0} and will be processed within {1} seconds'.format(
|
||||
member.name, cluster.config.data.get('loop_wait'))
|
||||
)
|
||||
else:
|
||||
click.echo('Failed: reload for member {0}, status code={1}, ({2})'.format(
|
||||
member.name, r.status_code, r.text)
|
||||
)
|
||||
|
||||
|
||||
@ctl.command('restart', help='Restart cluster member')
|
||||
@click.argument('cluster_name')
|
||||
@click.argument('member_names', nargs=-1)
|
||||
|
||||
@@ -91,6 +91,8 @@ class HTTPClient(object):
|
||||
data = '{' + ttl + '}'
|
||||
else:
|
||||
data = data[:-1] + ', ' + ttl + '}'
|
||||
if isinstance(params, list): # starting from v1.1.0 python-consul switched from `dict` to `list` for params
|
||||
params = {k: v for k, v in params}
|
||||
kwargs = {'retries': 0, 'preload_content': False, 'body': data}
|
||||
if method == 'get' and isinstance(params, dict) and 'index' in params:
|
||||
kwargs['timeout'] = (float(params['wait'][:-1]) if 'wait' in params else 300) + 1
|
||||
|
||||
+33
-23
@@ -57,6 +57,8 @@ class Ha(object):
|
||||
self.dcs = patroni.dcs
|
||||
self.cluster = None
|
||||
self.old_cluster = None
|
||||
self._is_leader = False
|
||||
self._is_leader_lock = RLock()
|
||||
self._was_paused = False
|
||||
self._leader_timeline = None
|
||||
self.recovering = False
|
||||
@@ -87,6 +89,14 @@ class Ha(object):
|
||||
def is_paused(self):
|
||||
return self.check_mode('pause')
|
||||
|
||||
def is_leader(self):
|
||||
with self._is_leader_lock:
|
||||
return self._is_leader
|
||||
|
||||
def set_is_leader(self, value):
|
||||
with self._is_leader_lock:
|
||||
self._is_leader = value
|
||||
|
||||
def load_cluster_from_dcs(self):
|
||||
cluster = self.dcs.get_cluster()
|
||||
|
||||
@@ -98,7 +108,9 @@ class Ha(object):
|
||||
self._leader_timeline = None if cluster.is_unlocked() else cluster.leader.timeline
|
||||
|
||||
def acquire_lock(self):
|
||||
return self.dcs.attempt_to_acquire_leader()
|
||||
ret = self.dcs.attempt_to_acquire_leader()
|
||||
self.set_is_leader(ret)
|
||||
return ret
|
||||
|
||||
def update_lock(self, write_leader_optime=False):
|
||||
last_operation = None
|
||||
@@ -108,6 +120,7 @@ class Ha(object):
|
||||
except Exception:
|
||||
logger.exception('Exception when called state_handler.last_operation()')
|
||||
ret = self.dcs.update_leader(last_operation)
|
||||
self.set_is_leader(ret)
|
||||
if ret:
|
||||
self.watchdog.keepalive()
|
||||
return ret
|
||||
@@ -201,11 +214,6 @@ class Ha(object):
|
||||
self._async_executor.run_async(self.state_handler.rewind, (self.cluster.leader,))
|
||||
return True
|
||||
|
||||
def _start_crash_recovery(self, msg):
|
||||
self._async_executor.schedule(msg)
|
||||
self._async_executor.run_async(self.state_handler.fix_cluster_state)
|
||||
return msg
|
||||
|
||||
def recover(self):
|
||||
# Postgres is not running and we will restart in standby mode. Watchdog is not needed until we promote.
|
||||
self.watchdog.disable()
|
||||
@@ -226,10 +234,12 @@ class Ha(object):
|
||||
timeout = None
|
||||
|
||||
data = self.state_handler.controldata()
|
||||
if data.get('Database cluster state') == 'in production' and not self._crash_recovery_executed and \
|
||||
(self.cluster.is_unlocked() or self.state_handler.can_rewind):
|
||||
if data.get('Database cluster state') in ('in production', 'shutting down', 'in crash recovery') and \
|
||||
not self._crash_recovery_executed and (self.cluster.is_unlocked() or self.state_handler.can_rewind):
|
||||
self._crash_recovery_executed = True
|
||||
return self._start_crash_recovery('doing crash recovery in a single user mode')
|
||||
self._async_executor.schedule('doing crash recovery in a single user mode')
|
||||
self._async_executor.run_async(self.state_handler.fix_cluster_state)
|
||||
return self._async_executor.scheduled_action
|
||||
|
||||
self.load_cluster_from_dcs()
|
||||
|
||||
@@ -244,13 +254,6 @@ class Ha(object):
|
||||
msg = "starting as a secondary"
|
||||
node_to_follow = self._get_node_to_follow(self.cluster)
|
||||
|
||||
# once we already tried to start postgres but failed, single user mode is a rescue in this case
|
||||
if self.recovering and not self.state_handler.rewind_executed \
|
||||
and not self._crash_recovery_executed and self.state_handler.can_rewind \
|
||||
and data.get('Database cluster state') not in ('shut down', 'shut down in recovery'):
|
||||
self.recovering = False
|
||||
return self._start_crash_recovery('fixing cluster state in a single user mode')
|
||||
|
||||
self.recovering = True
|
||||
|
||||
self._async_executor.schedule('restarting after failure')
|
||||
@@ -607,10 +610,10 @@ class Ha(object):
|
||||
PostgreSQL as quickly as possible without regard for data durability. May only be called synchronously.
|
||||
"""
|
||||
mode_control = {
|
||||
'offline': dict(stop='fast', checkpoint=False, release=False, offline=True, async=False),
|
||||
'graceful': dict(stop='fast', checkpoint=True, release=True, offline=False, async=False),
|
||||
'immediate': dict(stop='immediate', checkpoint=False, release=True, offline=False, async=True),
|
||||
'immediate-nolock': dict(stop='immediate', checkpoint=False, release=False, offline=False, async=True),
|
||||
'offline': dict(stop='fast', checkpoint=False, release=False, offline=True, async_req=False),
|
||||
'graceful': dict(stop='fast', checkpoint=True, release=True, offline=False, async_req=False),
|
||||
'immediate': dict(stop='immediate', checkpoint=False, release=True, offline=False, async_req=True),
|
||||
'immediate-nolock': dict(stop='immediate', checkpoint=False, release=False, offline=False, async_req=True),
|
||||
}[mode]
|
||||
|
||||
self.state_handler.trigger_check_diverged_lsn()
|
||||
@@ -630,7 +633,7 @@ class Ha(object):
|
||||
# FIXME: with mode offline called from DCS exception handler and handle_long_action_in_progress
|
||||
# there could be an async action already running, calling follow from here will lead
|
||||
# to racy state handler state updates.
|
||||
if mode_control['async']:
|
||||
if mode_control['async_req']:
|
||||
self._async_executor.schedule('starting after demotion')
|
||||
self._async_executor.run_async(self.state_handler.follow, (node_to_follow,))
|
||||
else:
|
||||
@@ -991,6 +994,7 @@ class Ha(object):
|
||||
logger.error('Cancelling bootstrap because watchdog activation failed')
|
||||
self.cancel_initialization()
|
||||
self.dcs.take_leader()
|
||||
self.set_is_leader(True)
|
||||
self.state_handler.call_nowait(ACTION_ON_START)
|
||||
self.load_cluster_from_dcs()
|
||||
return 'initialized a new cluster'
|
||||
@@ -1123,8 +1127,13 @@ class Ha(object):
|
||||
self.dcs.delete_leader()
|
||||
self.dcs.reset_cluster()
|
||||
return 'removed leader lock because postgres is not running'
|
||||
elif not (self.state_handler.rewind_executed or
|
||||
self.state_handler.need_rewind and self.state_handler.can_rewind):
|
||||
# Normally we don't start Postgres in a paused state. We make an exception for the demoted primary
|
||||
# that needs to be started after it had been stopped by demote. When there is no need to call rewind
|
||||
# the demote code follows through to starting Postgres right away, however, in the rewind case
|
||||
# it returns from demote and reaches this point to start PostgreSQL again after rewind. In that
|
||||
# case it makes no sense to continue to recover() unless rewind has finished successfully.
|
||||
elif (self.state_handler.rewind_failed or
|
||||
not (self.state_handler.need_rewind and self.state_handler.can_rewind)):
|
||||
return 'postgres is not running'
|
||||
|
||||
# try to start dead postgres
|
||||
@@ -1178,6 +1187,7 @@ class Ha(object):
|
||||
if not self.state_handler.is_running():
|
||||
if self.has_lock():
|
||||
self.dcs.delete_leader()
|
||||
self.touch_member()
|
||||
else:
|
||||
# XXX: what about when Patroni is started as the wrong user that has access to the watchdog device
|
||||
# but cannot shut down PostgreSQL. Root would be the obvious example. Would be nice to not kill the
|
||||
|
||||
+65
-13
@@ -179,6 +179,13 @@ class Postgresql(object):
|
||||
self._write_postgresql_conf() # we are "joining" already running postgres
|
||||
if self._replace_pg_hba():
|
||||
self.reload()
|
||||
elif self.role == 'master':
|
||||
self.set_role('demoted')
|
||||
|
||||
@property
|
||||
def _create_replica_methods(self):
|
||||
return (self.config.get('create_replica_methods', []) or
|
||||
self.config.get('create_replica_method', []))
|
||||
|
||||
@property
|
||||
def _configuration_to_save(self):
|
||||
@@ -330,10 +337,12 @@ class Postgresql(object):
|
||||
if new_value is None or not compare_values(r[3], unit, r[1], new_value):
|
||||
if r[4] == 'postmaster':
|
||||
pending_restart = True
|
||||
logger.info('Changed %s from %s to %s (restart required)', r[0], r[1], new_value)
|
||||
if config.get('use_unix_socket') and r[0] == 'unix_socket_directories'\
|
||||
or r[0] in ('listen_addresses', 'port'):
|
||||
local_connection_address_changed = True
|
||||
else:
|
||||
logger.info('Changed %s from %s to %s', r[0], r[1], new_value)
|
||||
conf_changed = True
|
||||
for param in changes:
|
||||
if param in server_parameters:
|
||||
@@ -344,11 +353,13 @@ class Postgresql(object):
|
||||
if not conf_changed:
|
||||
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
|
||||
break
|
||||
if not conf_changed:
|
||||
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
|
||||
break
|
||||
|
||||
@@ -370,7 +381,10 @@ class Postgresql(object):
|
||||
self._replace_pg_hba()
|
||||
|
||||
if conf_changed or hba_changed:
|
||||
logger.info('PostgreSQL configuration items changed, reloading configuration.')
|
||||
self.reload()
|
||||
elif not pending_restart:
|
||||
logger.info('No PostgreSQL configuration items changed, nothing to reload.')
|
||||
|
||||
self._is_leader_retry.deadline = self.retry.deadline = config['retry_timeout']/2.0
|
||||
|
||||
@@ -403,7 +417,7 @@ class Postgresql(object):
|
||||
|
||||
@property
|
||||
def sysid(self):
|
||||
if not self._sysid:
|
||||
if not self._sysid and not self.bootstrapping:
|
||||
data = self.controldata()
|
||||
self._sysid = data.get('Database system identifier', "")
|
||||
return self._sysid
|
||||
@@ -638,7 +652,7 @@ class Postgresql(object):
|
||||
""" go through the replication methods to see if there are ones
|
||||
that does not require a working replication connection.
|
||||
"""
|
||||
replica_methods = self.config.get('create_replica_method', [])
|
||||
replica_methods = self._create_replica_methods
|
||||
return any(self.replica_method_can_work_without_replication_connection(method) for method in replica_methods)
|
||||
|
||||
def create_replica(self, clone_member):
|
||||
@@ -653,7 +667,7 @@ class Postgresql(object):
|
||||
|
||||
# get list of replica methods from config.
|
||||
# If there is no configuration key, or no value is specified, use basebackup
|
||||
replica_methods = self.config.get('create_replica_method') or ['basebackup']
|
||||
replica_methods = self._create_replica_methods or ['basebackup']
|
||||
|
||||
if clone_member and clone_member.conn_url:
|
||||
r = clone_member.conn_kwargs(self._replication)
|
||||
@@ -818,6 +832,36 @@ class Postgresql(object):
|
||||
logger.warning("Timed out waiting for PostgreSQL to start")
|
||||
return False
|
||||
|
||||
def _build_effective_configuration(self):
|
||||
"""It might happen that the current value of one (or more) below parameters stored in
|
||||
the controldata is higher than the value stored in the global cluster configuration.
|
||||
|
||||
Example: max_connections in global configuration is 100, but in controldata
|
||||
`Current max_connections setting: 200`. If we try to start postgres with
|
||||
max_connections=100, it will immediately exit.
|
||||
As a workaround we will start it with the values from controldata and set `pending_restart`
|
||||
to true as an indicator that current values of parameters are not matching expectations."""
|
||||
|
||||
OPTIONS_MAPPING = {
|
||||
'max_connections': 'max_connections setting',
|
||||
'max_prepared_transactions': 'max_prepared_xacts setting',
|
||||
'max_locks_per_transaction': 'max_locks_per_xact setting'
|
||||
}
|
||||
|
||||
if self._major_version >= 90400:
|
||||
OPTIONS_MAPPING['max_worker_processes'] = 'max_worker_processes setting'
|
||||
|
||||
data = self.controldata()
|
||||
effective_configuration = self._server_parameters.copy()
|
||||
|
||||
for name, cname in OPTIONS_MAPPING.items():
|
||||
value = parse_int(effective_configuration[name])
|
||||
cvalue = parse_int(data[cname])
|
||||
if cvalue > value:
|
||||
effective_configuration[name] = cvalue
|
||||
self._pending_restart = True
|
||||
return effective_configuration
|
||||
|
||||
def start(self, timeout=None, block_callbacks=False, task=None):
|
||||
"""Start PostgreSQL
|
||||
|
||||
@@ -843,12 +887,13 @@ class Postgresql(object):
|
||||
self.set_state('starting')
|
||||
self._pending_restart = False
|
||||
|
||||
self._write_postgresql_conf()
|
||||
configuration = self._server_parameters if self.role == 'master' else self._build_effective_configuration()
|
||||
self._write_postgresql_conf(configuration)
|
||||
self.resolve_connection_addresses()
|
||||
self._replace_pg_hba()
|
||||
|
||||
options = ['--{0}={1}'.format(p, self._server_parameters[p]) for p in self.CMDLINE_OPTIONS
|
||||
if p in self._server_parameters and p != 'wal_keep_segments']
|
||||
options = ['--{0}={1}'.format(p, configuration[p]) for p in self.CMDLINE_OPTIONS
|
||||
if p in configuration and p != 'wal_keep_segments']
|
||||
|
||||
with self._cancellable_lock:
|
||||
if self._is_cancelled:
|
||||
@@ -1053,7 +1098,7 @@ class Postgresql(object):
|
||||
self.set_state('restart failed ({0})'.format(self.state))
|
||||
return ret
|
||||
|
||||
def _write_postgresql_conf(self):
|
||||
def _write_postgresql_conf(self, configuration=None):
|
||||
# rename the original configuration if it is necessary
|
||||
if 'custom_conf' not in self.config and not os.path.exists(self._postgresql_base_conf):
|
||||
os.rename(self._postgresql_conf, self._postgresql_base_conf)
|
||||
@@ -1061,7 +1106,7 @@ class Postgresql(object):
|
||||
with open(self._postgresql_conf, 'w') as f:
|
||||
f.write(self._CONFIG_WARNING_HEADER)
|
||||
f.write("include '{0}'\n\n".format(self.config.get('custom_conf') or self._postgresql_base_conf_name))
|
||||
for name, value in sorted(self._server_parameters.items()):
|
||||
for name, value in sorted((configuration or self._server_parameters).items()):
|
||||
if not self._running_custom_bootstrap or name != 'hba_file':
|
||||
f.write("{0} = '{1}'\n".format(name, value))
|
||||
# when we are doing custom bootstrap we assume that we don't know superuser password
|
||||
@@ -1164,7 +1209,7 @@ class Postgresql(object):
|
||||
""" return the contents of pg_controldata, or non-True value if pg_controldata call failed """
|
||||
result = {}
|
||||
# Don't try to call pg_controldata during backup restore
|
||||
if not self.bootstrapping and self._version_file_exists() and self.state != 'creating replica':
|
||||
if self._version_file_exists() and self.state != 'creating replica':
|
||||
try:
|
||||
data = subprocess.check_output([self._pgcommand('pg_controldata'), self._data_dir],
|
||||
env={'LANG': 'C', 'LC_ALL': 'C', 'PATH': os.environ['PATH']})
|
||||
@@ -1362,6 +1407,10 @@ class Postgresql(object):
|
||||
def rewind_executed(self):
|
||||
return self._rewind_state > REWIND_STATUS.NOT_NEED
|
||||
|
||||
@property
|
||||
def rewind_failed(self):
|
||||
return self._rewind_state == REWIND_STATUS.FAILED
|
||||
|
||||
def follow(self, member, timeout=None):
|
||||
primary_conninfo = self.primary_conninfo(member)
|
||||
change_role = self.role in ('master', 'demoted')
|
||||
@@ -1587,9 +1636,12 @@ $$""".format(name, ' '.join(options)), name, password, password)
|
||||
self.restart()
|
||||
else:
|
||||
self._replace_pg_hba()
|
||||
self.reload()
|
||||
time.sleep(1) # give a time to postgres to "reload" configuration files
|
||||
self.close_connection() # close connection to reconnect with a new password
|
||||
if self.pending_restart:
|
||||
self.restart()
|
||||
else:
|
||||
self.reload()
|
||||
time.sleep(1) # give a time to postgres to "reload" configuration files
|
||||
self.close_connection() # close connection to reconnect with a new password
|
||||
except Exception:
|
||||
logger.exception('post_bootstrap')
|
||||
task.complete(False)
|
||||
@@ -1622,7 +1674,7 @@ $$""".format(name, ' '.join(options)), name, password, password)
|
||||
|
||||
def basebackup(self, conn_url, env, options):
|
||||
# creates a replica data dir using pg_basebackup.
|
||||
# this is the default, built-in create_replica_method
|
||||
# this is the default, built-in create_replica_methods
|
||||
# tries twice, then returns failure (as 1)
|
||||
# uses "stream" as the xlog-method to avoid sync issues
|
||||
# supports additional user-supplied options, those are not validated
|
||||
|
||||
@@ -42,7 +42,8 @@ class PostmasterProcess(psutil.Process):
|
||||
try:
|
||||
start_time = int(self._postmaster_pid.get('start_time', 0))
|
||||
if start_time and abs(self.create_time() - start_time) > 3:
|
||||
logger.info('Too much difference between %s and %s', self.create_time(), start_time)
|
||||
logger.info('Process %s is not postmaster, too much difference between PID file start time %s and '
|
||||
'process start time %s', self.pid, self.create_time(), start_time)
|
||||
return False
|
||||
except ValueError:
|
||||
logger.warning('Garbage start time value in pid file: %r', self._postmaster_pid.get('start_time'))
|
||||
@@ -137,7 +138,7 @@ class PostmasterProcess(psutil.Process):
|
||||
# of init process to take care about postmaster.
|
||||
# In order to make everything portable we can't use fork&exec approach here, so we will call
|
||||
# ourselves and pass list of arguments which must be used to start postgres.
|
||||
env = {p: os.environ[p] for p in ('PATH', 'LC_ALL', 'LANG') if p in os.environ}
|
||||
env = {p: os.environ[p] for p in ('PATH', 'LD_LIBRARY_PATH', 'LC_ALL', 'LANG') if p in os.environ}
|
||||
try:
|
||||
proc = PostmasterProcess._from_pidfile(data_dir)
|
||||
if proc and not proc._is_postmaster_process():
|
||||
@@ -148,6 +149,7 @@ class PostmasterProcess(psutil.Process):
|
||||
# Important!!! Unlink of postmaster.pid isn't an option, because it has a lot of nasty race conditions.
|
||||
# Luckily there is a workaround to this problem, we can pass the pid from postmaster.pid
|
||||
# in the `PG_GRANDPARENT_PID` environment variable and postmaster will ignore it.
|
||||
logger.info("Telling pg_ctl that it is safe to ignore postmaster.pid for process %s", proc.pid)
|
||||
env['PG_GRANDPARENT_PID'] = str(proc.pid)
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = '1.4.4'
|
||||
__version__ = '1.4.5'
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ bootstrap:
|
||||
# wal_level: hot_standby
|
||||
# hot_standby: "on"
|
||||
# wal_keep_segments: 8
|
||||
# max_wal_senders: 5
|
||||
# max_replication_slots: 5
|
||||
# max_wal_senders: 10
|
||||
# max_replication_slots: 10
|
||||
# wal_log_hints: "on"
|
||||
# archive_mode: "on"
|
||||
# archive_timeout: 1800s
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ bootstrap:
|
||||
# wal_level: hot_standby
|
||||
# hot_standby: "on"
|
||||
# wal_keep_segments: 8
|
||||
# max_wal_senders: 5
|
||||
# max_replication_slots: 5
|
||||
# max_wal_senders: 10
|
||||
# max_replication_slots: 10
|
||||
# wal_log_hints: "on"
|
||||
# archive_mode: "on"
|
||||
# archive_timeout: 1800s
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ bootstrap:
|
||||
# wal_level: hot_standby
|
||||
# hot_standby: "on"
|
||||
# wal_keep_segments: 8
|
||||
# max_wal_senders: 5
|
||||
# max_replication_slots: 5
|
||||
# max_wal_senders: 10
|
||||
# max_replication_slots: 10
|
||||
# wal_log_hints: "on"
|
||||
# archive_mode: "on"
|
||||
# archive_timeout: 1800s
|
||||
|
||||
+1
-1
@@ -13,4 +13,4 @@ tzlocal
|
||||
python-dateutil
|
||||
psutil
|
||||
cdiff
|
||||
kubernetes==3.0.0
|
||||
kubernetes>=2.0.0,<=7.0.0,!=4.0.*,!=5.0.*
|
||||
|
||||
@@ -53,6 +53,8 @@ CLASSIFIERS = [
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
]
|
||||
|
||||
|
||||
+19
-5
@@ -51,6 +51,10 @@ class MockHa(object):
|
||||
state_handler = MockPostgresql()
|
||||
watchdog = MockWatchdog()
|
||||
|
||||
@staticmethod
|
||||
def is_leader():
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def reinitialize(_):
|
||||
return 'reinitialize'
|
||||
@@ -123,16 +127,14 @@ class MockRequest(object):
|
||||
|
||||
class MockRestApiServer(RestApiServer):
|
||||
|
||||
def __init__(self, Handler, request):
|
||||
def __init__(self, Handler, request, config=None):
|
||||
self.socket = 0
|
||||
self.serve_forever = Mock()
|
||||
BaseHTTPServer.HTTPServer.__init__ = Mock()
|
||||
MockRestApiServer._BaseServer__is_shut_down = Mock()
|
||||
MockRestApiServer._BaseServer__shutdown_request = True
|
||||
config = {'listen': '127.0.0.1:8008', 'auth': 'test:test'}
|
||||
config = config or {'listen': '127.0.0.1:8008', 'auth': 'test:test', 'certfile': 'dumb'}
|
||||
super(MockRestApiServer, self).__init__(MockPatroni(), config)
|
||||
config['certfile'] = 'dumb'
|
||||
self.reload_config(config)
|
||||
Handler(MockRequest(request), ('0.0.0.0', 8080), self)
|
||||
|
||||
|
||||
@@ -154,7 +156,7 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
MockRestApiServer(RestApiHandler, 'GET /synchronous')
|
||||
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'replica'})):
|
||||
MockRestApiServer(RestApiHandler, 'GET /asynchronous')
|
||||
MockPatroni.dcs.cluster.leader.name = MockPostgresql.name
|
||||
MockPatroni.ha.is_leader = Mock(return_value=True)
|
||||
MockRestApiServer(RestApiHandler, 'GET /replica')
|
||||
MockPatroni.dcs.cluster = None
|
||||
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'master'})):
|
||||
@@ -380,3 +382,15 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
post = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: '
|
||||
MockRestApiServer(RestApiHandler, post + '14\n\n{"leader":"1"}')
|
||||
MockRestApiServer(RestApiHandler, post + '37\n\n{"candidate":"2","scheduled_at": "1"}')
|
||||
|
||||
|
||||
@patch('ssl.wrap_socket', Mock(return_value=0))
|
||||
class TestRestApiServer(unittest.TestCase):
|
||||
|
||||
def test_reload_config(self):
|
||||
bad_config = {'listen': 'foo'}
|
||||
self.assertRaises(ValueError, MockRestApiServer, None, '', bad_config)
|
||||
srv = MockRestApiServer(lambda a1, a2, a3: None, '')
|
||||
self.assertRaises(ValueError, srv.reload_config, bad_config)
|
||||
self.assertRaises(ValueError, srv.reload_config, {})
|
||||
srv.reload_config({'listen': '127.0.0.2:8008'})
|
||||
|
||||
+1
-10
@@ -15,9 +15,9 @@ from patroni.utils import tzutc
|
||||
from test_etcd import socket_getaddrinfo, etcd_read, etcd_write, requests_get
|
||||
from test_postgresql import psycopg2_connect, MockPostmaster
|
||||
|
||||
|
||||
SYSID = '12345678901'
|
||||
|
||||
|
||||
def true(*args, **kwargs):
|
||||
return True
|
||||
|
||||
@@ -207,15 +207,6 @@ class TestHa(unittest.TestCase):
|
||||
self.ha.cluster = get_cluster_initialized_with_leader()
|
||||
self.assertEquals(self.ha.run_cycle(), 'running pg_rewind from leader')
|
||||
|
||||
@patch.object(Postgresql, 'can_rewind', PropertyMock(return_value=True))
|
||||
@patch.object(Postgresql, 'fix_cluster_state', Mock())
|
||||
def test_single_user_after_recover_failed(self):
|
||||
self.p.controldata = lambda: {'Database cluster state': 'in recovery', 'Database system identifier': SYSID}
|
||||
self.p.is_running = false
|
||||
self.p.follow = false
|
||||
self.assertEquals(self.ha.run_cycle(), 'starting as a secondary')
|
||||
self.assertEquals(self.ha.run_cycle(), 'fixing cluster state in a single user mode')
|
||||
|
||||
@patch('sys.exit', return_value=1)
|
||||
@patch('patroni.ha.Ha.sysid_valid', MagicMock(return_value=True))
|
||||
def test_sysid_no_match(self, exit_mock):
|
||||
|
||||
@@ -103,6 +103,7 @@ class TestPatroni(unittest.TestCase):
|
||||
|
||||
@patch('patroni.config.Config.save_cache', Mock())
|
||||
@patch('patroni.config.Config.reload_local_configuration', Mock(return_value=True))
|
||||
@patch('patroni.ha.Ha.is_leader', Mock(return_value=True))
|
||||
@patch.object(Postgresql, 'state', PropertyMock(return_value='running'))
|
||||
@patch.object(Postgresql, 'data_directory_empty', Mock(return_value=False))
|
||||
def test_run(self):
|
||||
@@ -151,3 +152,7 @@ class TestPatroni(unittest.TestCase):
|
||||
self.assertTrue(self.p.nosync)
|
||||
self.p.tags['nosync'] = None
|
||||
self.assertFalse(self.p.nosync)
|
||||
|
||||
def test_shutdown(self):
|
||||
self.p.api.shutdown = Mock(side_effect=Exception)
|
||||
self.p.shutdown()
|
||||
|
||||
@@ -14,7 +14,7 @@ from patroni.postgresql import Postgresql, STATE_REJECT, STATE_NO_RESPONSE
|
||||
from patroni.postmaster import PostmasterProcess
|
||||
from patroni.utils import RetryFailedError
|
||||
from six.moves import builtins
|
||||
from threading import Thread
|
||||
from threading import Thread, current_thread
|
||||
|
||||
|
||||
class MockCursor(object):
|
||||
@@ -420,14 +420,14 @@ class TestPostgresql(unittest.TestCase):
|
||||
def test_create_replica(self, mock_cancellable_subprocess_call):
|
||||
self.p.delete_trigger_file = Mock(side_effect=OSError)
|
||||
|
||||
self.p.config['create_replica_method'] = ['wale', 'basebackup']
|
||||
self.p.config['create_replica_methods'] = ['wale', 'basebackup']
|
||||
self.p.config['wale'] = {'command': 'foo'}
|
||||
mock_cancellable_subprocess_call.return_value = 0
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
del self.p.config['wale']
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
|
||||
self.p.config['create_replica_method'] = ['basebackup']
|
||||
self.p.config['create_replica_methods'] = ['basebackup']
|
||||
self.p.config['basebackup'] = [{'max_rate': '100M'}, 'no-sync']
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
|
||||
@@ -448,7 +448,7 @@ class TestPostgresql(unittest.TestCase):
|
||||
self.p.config['basebackup'] = {"foo": "bar"}
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
|
||||
self.p.config['create_replica_method'] = ['wale', 'basebackup']
|
||||
self.p.config['create_replica_methods'] = ['wale', 'basebackup']
|
||||
del self.p.config['basebackup']
|
||||
mock_cancellable_subprocess_call.return_value = 1
|
||||
self.assertEquals(self.p.create_replica(self.leader), 1)
|
||||
@@ -465,6 +465,31 @@ class TestPostgresql(unittest.TestCase):
|
||||
self.p.cancel()
|
||||
self.assertEquals(self.p.create_replica(self.leader), 1)
|
||||
|
||||
@patch('time.sleep', Mock())
|
||||
@patch.object(Postgresql, 'cancellable_subprocess_call')
|
||||
@patch.object(Postgresql, 'remove_data_directory', Mock(return_value=True))
|
||||
def test_create_replica_old_format(self, mock_cancellable_subprocess_call):
|
||||
""" The same test as before but with old 'create_replica_method'
|
||||
to test backward compatibility
|
||||
"""
|
||||
self.p.delete_trigger_file = Mock(side_effect=OSError)
|
||||
|
||||
self.p.config['create_replica_method'] = ['wale', 'basebackup']
|
||||
self.p.config['wale'] = {'command': 'foo'}
|
||||
mock_cancellable_subprocess_call.return_value = 0
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
del self.p.config['wale']
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
|
||||
self.p.config['create_replica_method'] = ['basebackup']
|
||||
self.p.config['basebackup'] = [{'max_rate': '100M'}, 'no-sync']
|
||||
self.assertEquals(self.p.create_replica(self.leader), 0)
|
||||
|
||||
self.p.config['create_replica_method'] = ['wale', 'basebackup']
|
||||
del self.p.config['basebackup']
|
||||
mock_cancellable_subprocess_call.return_value = 1
|
||||
self.assertEquals(self.p.create_replica(self.leader), 1)
|
||||
|
||||
def test_basebackup(self):
|
||||
self.p.cancel()
|
||||
self.p.basebackup(None, None, {'foo': 'bar'})
|
||||
@@ -635,6 +660,12 @@ class TestPostgresql(unittest.TestCase):
|
||||
self.p.post_bootstrap({}, task)
|
||||
self.assertTrue(task.result)
|
||||
|
||||
self.p.bootstrap(config)
|
||||
with patch.object(Postgresql, 'pending_restart', PropertyMock(return_value=True)), \
|
||||
patch.object(Postgresql, 'restart', Mock()) as mock_restart:
|
||||
self.p.post_bootstrap({}, task)
|
||||
mock_restart.assert_called_once()
|
||||
|
||||
self.p.bootstrap(config)
|
||||
self.p.set_state('stopped')
|
||||
self.p.reload_config({'authentication': {'superuser': {'username': 'p', 'password': 'p'},
|
||||
@@ -790,10 +821,12 @@ class TestPostgresql(unittest.TestCase):
|
||||
|
||||
def test_wait_for_startup(self):
|
||||
state = {'sleeps': 0, 'num_rejects': 0, 'final_return': 0}
|
||||
self.__thread_ident = current_thread().ident
|
||||
|
||||
def increment_sleeps(*args):
|
||||
print("Sleep")
|
||||
state['sleeps'] += 1
|
||||
if current_thread().ident == self.__thread_ident:
|
||||
print("Sleep")
|
||||
state['sleeps'] += 1
|
||||
|
||||
def isready_return(*args):
|
||||
ret = 1 if state['sleeps'] < state['num_rejects'] else state['final_return']
|
||||
@@ -969,3 +1002,14 @@ class TestPostgresql(unittest.TestCase):
|
||||
self.p.cancel()
|
||||
type(self.p._cancellable).returncode = PropertyMock(side_effect=[None, -15])
|
||||
self.p.cancel()
|
||||
|
||||
@patch.object(Postgresql, 'get_postgres_role_from_data_directory', Mock(return_value='replica'))
|
||||
def test__build_effective_configuration(self):
|
||||
with patch.object(Postgresql, 'controldata',
|
||||
Mock(return_value={'max_connections setting': '200',
|
||||
'max_worker_processes setting': '20',
|
||||
'max_prepared_xacts setting': '100',
|
||||
'max_locks_per_xact setting': '100'})):
|
||||
self.p.cancel()
|
||||
self.assertFalse(self.p.start())
|
||||
self.assertTrue(self.p.pending_restart)
|
||||
|
||||
@@ -7,6 +7,7 @@ from patroni.scripts import wale_restore
|
||||
from patroni.scripts.wale_restore import WALERestore, main as _main, get_major_version
|
||||
from six.moves import builtins
|
||||
from test_postgresql import MockConnect, psycopg2_connect
|
||||
from threading import current_thread
|
||||
|
||||
|
||||
wale_output_header = (
|
||||
@@ -42,6 +43,13 @@ class TestWALERestore(unittest.TestCase):
|
||||
'/etc', 100, 100, 1, 0, WALE_TEST_RETRIES)
|
||||
|
||||
def test_should_use_s3_to_create_replica(self):
|
||||
self.__thread_ident = current_thread().ident
|
||||
sleeps = [0]
|
||||
|
||||
def mock_sleep(*args):
|
||||
if current_thread().ident == self.__thread_ident:
|
||||
sleeps[0] += 1
|
||||
|
||||
self.assertTrue(self.wale_restore.should_use_s3_to_create_replica())
|
||||
with patch.object(MockConnect, 'server_version', PropertyMock(return_value=100000)):
|
||||
self.assertTrue(self.wale_restore.should_use_s3_to_create_replica())
|
||||
@@ -55,13 +63,11 @@ class TestWALERestore(unittest.TestCase):
|
||||
|
||||
self.assertFalse(self.wale_restore.should_use_s3_to_create_replica())
|
||||
|
||||
with patch('time.sleep', Mock(return_value=None)) as mock_sleep:
|
||||
with patch('time.sleep', mock_sleep):
|
||||
self.wale_restore.no_master = 1
|
||||
assert self.wale_restore.should_use_s3_to_create_replica()
|
||||
self.assertTrue(self.wale_restore.should_use_s3_to_create_replica())
|
||||
# verify retries
|
||||
mock_sleep.assert_has_calls(
|
||||
[((wale_restore.RETRY_SLEEP_INTERVAL,),)] * WALE_TEST_RETRIES
|
||||
)
|
||||
self.assertEqual(sleeps[0], WALE_TEST_RETRIES)
|
||||
|
||||
self.wale_restore.master_connection = ''
|
||||
self.assertTrue(self.wale_restore.should_use_s3_to_create_replica())
|
||||
@@ -104,13 +110,20 @@ class TestWALERestore(unittest.TestCase):
|
||||
|
||||
@patch('sys.exit', Mock())
|
||||
def test_main(self):
|
||||
self.__thread_ident = current_thread().ident
|
||||
sleeps = [0]
|
||||
|
||||
def mock_sleep(*args):
|
||||
if current_thread().ident == self.__thread_ident:
|
||||
sleeps[0] += 1
|
||||
|
||||
with patch.object(WALERestore, 'run', Mock(return_value=0)):
|
||||
self.assertEqual(_main(), 0)
|
||||
|
||||
with patch.object(WALERestore, 'run', Mock(return_value=1)), \
|
||||
patch('time.sleep', Mock(return_value=None)) as mock_sleep:
|
||||
patch('time.sleep', mock_sleep):
|
||||
self.assertEqual(_main(), 1)
|
||||
assert mock_sleep.call_count == WALE_TEST_RETRIES
|
||||
self.assertTrue(sleeps[0], WALE_TEST_RETRIES)
|
||||
|
||||
@patch('os.path.isfile', Mock(return_value=True))
|
||||
def test_get_major_version(self):
|
||||
|
||||
Reference in New Issue
Block a user