mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-31 16:49:46 +00:00
Compare commits
+1
-1
@@ -5,7 +5,7 @@ python:
|
|||||||
- "3.4" # 2.7 and 3.5 are preinstalled by default
|
- "3.4" # 2.7 and 3.5 are preinstalled by default
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- ETCDVERSION=3.0.15 ZKVERSION=3.4.9 CONSULVERSION=0.7.2
|
- ETCDVERSION=3.0.17 ZKVERSION=3.4.9 CONSULVERSION=0.7.4
|
||||||
- PYVERSIONS="2.7 3.4 3.5"
|
- PYVERSIONS="2.7 3.4 3.5"
|
||||||
matrix:
|
matrix:
|
||||||
- TEST_SUITE="python setup.py"
|
- TEST_SUITE="python setup.py"
|
||||||
|
|||||||
+10
-25
@@ -1,33 +1,19 @@
|
|||||||
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
|
## 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
|
## It has all the necessary components to play/debug with a single node appliance, running etcd
|
||||||
FROM ubuntu:16.04
|
FROM postgres:9.6
|
||||||
MAINTAINER Alexander Kukushkin <[email protected]>
|
MAINTAINER Alexander Kukushkin <[email protected]>
|
||||||
|
|
||||||
RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend \
|
RUN echo 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";' > /etc/apt/apt.conf.d/01norecommend \
|
||||||
&& echo 'APT::Install-Suggests "0";' >> /etc/apt/apt.conf.d/01norecommend
|
&& apt-get update -y \
|
||||||
|
|
||||||
ENV PGVERSION 9.6
|
|
||||||
ENV PATH /usr/lib/postgresql/${PGVERSION}/bin:$PATH
|
|
||||||
RUN apt-get update -y \
|
|
||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
&& apt-get install -y curl jq haproxy zookeeper python-psycopg2 python-yaml python-requests \
|
&& apt-get install -y curl jq haproxy python-psycopg2 python-yaml python-requests \
|
||||||
python-six python-click python-dateutil python-tzlocal python-urllib3 python-dnspython \
|
python-six python-dateutil python-urllib3 python-dnspython \
|
||||||
python-pip python-setuptools python-kazoo python-prettytable python-wheel python \
|
python-pip python-setuptools python-kazoo python-prettytable python-wheel python \
|
||||||
|
|
||||||
&& export DISTRIB_CODENAME=$(sed -n 's/DISTRIB_CODENAME=//p' /etc/lsb-release) \
|
&& pip install python-etcd==0.4.3 python-consul==0.7.0 click tzlocal --upgrade \
|
||||||
&& echo "deb http://apt.postgresql.org/pub/repos/apt/ ${DISTRIB_CODENAME}-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
|
||||||
&& echo "deb-src http://apt.postgresql.org/pub/repos/apt/ ${DISTRIB_CODENAME}-pgdg main" >> /etc/apt/sources.list.d/pgdg.list \
|
|
||||||
&& curl -s -o - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
|
|
||||||
|
|
||||||
## Make sure we have a en_US.UTF-8 locale available
|
&& mkdir -p /home/postgres \
|
||||||
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
|
&& chown postgres:postgres /home/postgres \
|
||||||
|
|
||||||
&& apt-get update -y \
|
|
||||||
&& apt-get install -y postgresql-contrib-${PGVERSION} \
|
|
||||||
|
|
||||||
# Remove the default cluster, which Debian stupidly starts right after installation of the packages
|
|
||||||
&& pg_dropcluster --stop ${PGVERSION} main \
|
|
||||||
&& pip install python-etcd==0.4.3 python-consul==0.6.1 --upgrade \
|
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
&& apt-get remove -y python-pip python-setuptools \
|
&& apt-get remove -y python-pip python-setuptools \
|
||||||
@@ -35,7 +21,7 @@ RUN apt-get update -y \
|
|||||||
&& apt-get clean -y \
|
&& apt-get clean -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* /root/.cache
|
&& rm -rf /var/lib/apt/lists/* /root/.cache
|
||||||
|
|
||||||
ENV ETCDVERSION 3.0.15
|
ENV ETCDVERSION 3.1.2
|
||||||
RUN curl -L https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-amd64.tar.gz \
|
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
|
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl
|
||||||
|
|
||||||
@@ -50,8 +36,7 @@ RUN ln -s /patronictl.py /usr/local/bin/patronictl
|
|||||||
|
|
||||||
### Setting up a simple script that will serve as an entrypoint
|
### Setting up a simple script that will serve as an entrypoint
|
||||||
RUN mkdir /data/ && touch /pgpass /patroni.yml \
|
RUN mkdir /data/ && touch /pgpass /patroni.yml \
|
||||||
&& chown postgres:postgres -R /patroni/ /data/ /pgpass /patroni.yml /etc/haproxy /var/run/ /var/lib/ /var/log/ \
|
&& chown postgres:postgres -R /patroni/ /data/ /pgpass /patroni.yml /etc/haproxy /var/run/ /var/lib/ /var/log/
|
||||||
&& echo 1 > /etc/zookeeper/conf/myid
|
|
||||||
|
|
||||||
EXPOSE 2379 5432 8008
|
EXPOSE 2379 5432 8008
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -3,7 +3,7 @@
|
|||||||
Patroni: A Template for PostgreSQL HA with ZooKeeper, etcd or Consul
|
Patroni: A Template for PostgreSQL HA with ZooKeeper, etcd or Consul
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
|
|
||||||
You can find a version of this documentation that is searchable and also easier to navigagate at `patroni.readthedocs.io <https://patroni.readthedocs.io>`__.
|
You can find a version of this documentation that is searchable and also easier to navigate at `patroni.readthedocs.io <https://patroni.readthedocs.io>`__.
|
||||||
|
|
||||||
|
|
||||||
There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation <https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling>`__.
|
There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation <https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling>`__.
|
||||||
@@ -12,7 +12,7 @@ Patroni is a template for you to create your own customized, high-availability s
|
|||||||
|
|
||||||
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely.
|
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely.
|
||||||
|
|
||||||
**Note to Kubernetes users**: We're currently developing Patroni to be as useful as possible for teams running Kubernetes on top of Google Compute Engine; Patroni can be the HA solution for Postgres in such an environment. To this end, we've created a `Helm Chart <https://github.com/kubernetes/charts/tree/master/incubator/patroni>`__ that enables you to deploy a five-node Patroni cluster using a Kubernetes PetSet.
|
**Note to Kubernetes users**: We're currently developing Patroni to be as useful as possible for teams running Kubernetes on top of Google Compute Engine; Patroni can be the HA solution for Postgres in such an environment. To this end, there is a `Helm chart <https://github.com/kubernetes/charts/tree/master/incubator/patroni>`__ that uses Patroni and `Spilo <https://github.com/zalando/spilo/>`__ to provision a five-node PostgreSQL HA cluster in a Kubernetes+GCE environment. (The Helm chart deploys Spilo Docker images, not just "bare" Patroni.)
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
:local:
|
:local:
|
||||||
@@ -29,6 +29,7 @@ For an example of a Docker-based deployment with Patroni, see `Spilo <https://gi
|
|||||||
|
|
||||||
For additional background info, see:
|
For additional background info, see:
|
||||||
|
|
||||||
|
* `Elephants on Automatic: HA Clustered PostgreSQL with Helm <https://www.youtube.com/watch?v=CftcVhFMGSY>`_, talk by Josh Berkus and Oleksii Kliukin at KubeCon Berlin 2017
|
||||||
* `PostgreSQL HA with Kubernetes and Patroni <https://www.youtube.com/watch?v=iruaCgeG7qs>`__, talk by Josh Berkus at KubeCon 2016 (video)
|
* `PostgreSQL HA with Kubernetes and Patroni <https://www.youtube.com/watch?v=iruaCgeG7qs>`__, talk by Josh Berkus at KubeCon 2016 (video)
|
||||||
* `Feb. 2016 Zalando Tech blog post <https://tech.zalando.de/blog/zalandos-patroni-a-template-for-high-availability-postgresql/>`__
|
* `Feb. 2016 Zalando Tech blog post <https://tech.zalando.de/blog/zalandos-patroni-a-template-for-high-availability-postgresql/>`__
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ Go `here <https://github.com/zalando/patroni/blob/master/docs/ENVIRONMENT.rst>`_
|
|||||||
Replication Choices
|
Replication Choices
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Patroni uses Postgres' streaming replication, which is asynchronous by default. Patroni's asynchronous replication configuration allows for ``maximum_lag_on_failover`` settings. This setting ensures failover will not occur if a follower is more than a certain number of bytes behind the leader. This setting should be increased or decreased based on business requirements. It's also possible to use synchronous replication for better durability guarantees. See `replication modes documentation <https://github.com/zalando/patroni/blob/master/docs/replication_modes.rst>` for details.
|
Patroni uses Postgres' streaming replication, which is asynchronous by default. Patroni's asynchronous replication configuration allows for ``maximum_lag_on_failover`` settings. This setting ensures failover will not occur if a follower is more than a certain number of bytes behind the leader. This setting should be increased or decreased based on business requirements. It's also possible to use synchronous replication for better durability guarantees. See `replication modes documentation <https://github.com/zalando/patroni/blob/master/docs/replication_modes.rst>`__ for details.
|
||||||
|
|
||||||
===============================
|
===============================
|
||||||
Applications Should Not Use Superusers
|
Applications Should Not Use Superusers
|
||||||
|
|||||||
+2
-2
@@ -50,8 +50,8 @@ haproxy:
|
|||||||
links:
|
links:
|
||||||
- patroni_etcd:patroni_etcd
|
- patroni_etcd:patroni_etcd
|
||||||
ports:
|
ports:
|
||||||
- "5000"
|
- "5000:5000"
|
||||||
- "5001"
|
- "5001:5001"
|
||||||
environment:
|
environment:
|
||||||
PATRONI_ETCD_HOST: patroni_etcd:2379
|
PATRONI_ETCD_HOST: patroni_etcd:2379
|
||||||
PATRONI_SCOPE: testcluster
|
PATRONI_SCOPE: testcluster
|
||||||
|
|||||||
+36
-18
@@ -296,19 +296,6 @@ class Client(etcd.Client):
|
|||||||
self._machines_cache_updated = time.time()
|
self._machines_cache_updated = time.time()
|
||||||
|
|
||||||
|
|
||||||
def catch_etcd_errors(func):
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs) is not None
|
|
||||||
except (RetryFailedError, etcd.EtcdException):
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
logger.exception("")
|
|
||||||
raise EtcdError("unexpected error")
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class Etcd(AbstractDCS):
|
class Etcd(AbstractDCS):
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@@ -320,10 +307,36 @@ class Etcd(AbstractDCS):
|
|||||||
etcd.EtcdEventIndexCleared))
|
etcd.EtcdEventIndexCleared))
|
||||||
self._client = self.get_etcd_client(config)
|
self._client = self.get_etcd_client(config)
|
||||||
self.__do_not_watch = False
|
self.__do_not_watch = False
|
||||||
|
self._has_failed = False
|
||||||
|
|
||||||
def retry(self, *args, **kwargs):
|
def retry(self, *args, **kwargs):
|
||||||
return self._retry.copy()(*args, **kwargs)
|
return self._retry.copy()(*args, **kwargs)
|
||||||
|
|
||||||
|
def _handle_exception(self, e, name='', do_sleep=False, raise_ex=None):
|
||||||
|
if not self._has_failed:
|
||||||
|
logger.exception(name)
|
||||||
|
else:
|
||||||
|
logger.error(e)
|
||||||
|
if do_sleep:
|
||||||
|
time.sleep(1)
|
||||||
|
self._has_failed = True
|
||||||
|
if isinstance(raise_ex, Exception):
|
||||||
|
raise raise_ex
|
||||||
|
|
||||||
|
def catch_etcd_errors(func):
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
retval = func(self, *args, **kwargs) is not None
|
||||||
|
self._has_failed = False
|
||||||
|
return retval
|
||||||
|
except (RetryFailedError, etcd.EtcdException) as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_exception(e, raise_ex=EtcdError('unexpected error'))
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_etcd_client(config):
|
def get_etcd_client(config):
|
||||||
if 'proxy' in config:
|
if 'proxy' in config:
|
||||||
@@ -448,9 +461,9 @@ class Etcd(AbstractDCS):
|
|||||||
self._cluster = Cluster(initialize, config, leader, last_leader_operation, members, failover, sync)
|
self._cluster = Cluster(initialize, config, leader, last_leader_operation, members, failover, sync)
|
||||||
except etcd.EtcdKeyNotFound:
|
except etcd.EtcdKeyNotFound:
|
||||||
self._cluster = Cluster(None, None, None, None, [], None, None)
|
self._cluster = Cluster(None, None, None, None, [], None, None)
|
||||||
except:
|
except Exception as e:
|
||||||
logger.exception('get_cluster')
|
self._handle_exception(e, 'get_cluster', raise_ex=EtcdError('Etcd is not responding properly'))
|
||||||
raise EtcdError('Etcd is not responding properly')
|
self._has_failed = False
|
||||||
|
|
||||||
@catch_etcd_errors
|
@catch_etcd_errors
|
||||||
def touch_member(self, data, ttl=None, permanent=False):
|
def touch_member(self, data, ttl=None, permanent=False):
|
||||||
@@ -524,14 +537,19 @@ class Etcd(AbstractDCS):
|
|||||||
while timeout >= 1: # when timeout is too small urllib3 doesn't have enough time to connect
|
while timeout >= 1: # when timeout is too small urllib3 doesn't have enough time to connect
|
||||||
try:
|
try:
|
||||||
self._client.watch(self.leader_path, index=leader_index, timeout=timeout + 0.5)
|
self._client.watch(self.leader_path, index=leader_index, timeout=timeout + 0.5)
|
||||||
|
self._has_failed = False
|
||||||
# Synchronous work of all cluster members with etcd is less expensive
|
# Synchronous work of all cluster members with etcd is less expensive
|
||||||
# than reestablishing http connection every time from every replica.
|
# than reestablishing http connection every time from every replica.
|
||||||
return True
|
return True
|
||||||
except etcd.EtcdWatchTimedOut:
|
except etcd.EtcdWatchTimedOut:
|
||||||
self._client.http.clear()
|
self._client.http.clear()
|
||||||
|
self._has_failed = False
|
||||||
return False
|
return False
|
||||||
except etcd.EtcdException:
|
except (etcd.EtcdEventIndexCleared, etcd.EtcdWatcherCleared): # Watch failed
|
||||||
logger.exception('watch')
|
self._has_failed = False
|
||||||
|
return True # leave the loop, because watch with the same parameters will fail anyway
|
||||||
|
except etcd.EtcdException as e:
|
||||||
|
self._handle_exception(e, 'watch', True)
|
||||||
|
|
||||||
timeout = end_time - time.time()
|
timeout = end_time - time.time()
|
||||||
|
|
||||||
|
|||||||
@@ -886,6 +886,11 @@ class Ha(object):
|
|||||||
|
|
||||||
# is data directory empty?
|
# is data directory empty?
|
||||||
if self.state_handler.data_directory_empty():
|
if self.state_handler.data_directory_empty():
|
||||||
|
# is this instance the leader?
|
||||||
|
if self.has_lock():
|
||||||
|
self.release_leader_key_voluntarily()
|
||||||
|
return 'released leader key voluntarily as data dir empty and currently leader'
|
||||||
|
|
||||||
return self.bootstrap() # new node
|
return self.bootstrap() # new node
|
||||||
# "bootstrap", but data directory is not empty
|
# "bootstrap", but data directory is not empty
|
||||||
elif not self.sysid_valid(self.cluster.initialize) and self.cluster.is_unlocked() and not self.is_paused():
|
elif not self.sysid_valid(self.cluster.initialize) and self.cluster.is_unlocked() and not self.is_paused():
|
||||||
|
|||||||
+17
-12
@@ -116,6 +116,7 @@ class Postgresql(object):
|
|||||||
self._trigger_file = config.get('recovery_conf', {}).get('trigger_file') or 'promote'
|
self._trigger_file = config.get('recovery_conf', {}).get('trigger_file') or 'promote'
|
||||||
self._trigger_file = os.path.abspath(os.path.join(self._data_dir, self._trigger_file))
|
self._trigger_file = os.path.abspath(os.path.join(self._data_dir, self._trigger_file))
|
||||||
|
|
||||||
|
self._connection_lock = Lock()
|
||||||
self._connection = None
|
self._connection = None
|
||||||
self._cursor_holder = None
|
self._cursor_holder = None
|
||||||
self._sysid = None
|
self._sysid = None
|
||||||
@@ -177,6 +178,8 @@ class Postgresql(object):
|
|||||||
parameters.pop('synchronous_standby_names', None)
|
parameters.pop('synchronous_standby_names', None)
|
||||||
else:
|
else:
|
||||||
parameters['synchronous_standby_names'] = self._synchronous_standby_names
|
parameters['synchronous_standby_names'] = self._synchronous_standby_names
|
||||||
|
if self._major_version >= 9.6 and parameters['wal_level'] == 'hot_standby':
|
||||||
|
parameters['wal_level'] = 'replica'
|
||||||
return {k: v for k, v in parameters.items() if not self._major_version or
|
return {k: v for k, v in parameters.items() if not self._major_version or
|
||||||
self._major_version >= self.CMDLINE_OPTIONS.get(k, (0, 1, 9.1))[2]}
|
self._major_version >= self.CMDLINE_OPTIONS.get(k, (0, 1, 9.1))[2]}
|
||||||
|
|
||||||
@@ -249,8 +252,6 @@ class Postgresql(object):
|
|||||||
elif r[0] in changes:
|
elif r[0] in changes:
|
||||||
unit = changes['wal_segment_size'] if r[0] in ('min_wal_size', 'max_wal_size') else r[2]
|
unit = changes['wal_segment_size'] if r[0] in ('min_wal_size', 'max_wal_size') else r[2]
|
||||||
new_value = changes.pop(r[0])
|
new_value = changes.pop(r[0])
|
||||||
if self._major_version >= 9.6 and r[0] == 'wal_level' and new_value == 'hot_standby':
|
|
||||||
new_value = 'replica'
|
|
||||||
if new_value is None or not compare_values(r[3], unit, r[1], new_value):
|
if new_value is None or not compare_values(r[3], unit, r[1], new_value):
|
||||||
if r[4] == 'postmaster':
|
if r[4] == 'postmaster':
|
||||||
pending_restart = True
|
pending_restart = True
|
||||||
@@ -351,10 +352,11 @@ class Postgresql(object):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def connection(self):
|
def connection(self):
|
||||||
if not self._connection or self._connection.closed != 0:
|
with self._connection_lock:
|
||||||
self._connection = psycopg2.connect(**self._local_connect_kwargs)
|
if not self._connection or self._connection.closed != 0:
|
||||||
self._connection.autocommit = True
|
self._connection = psycopg2.connect(**self._local_connect_kwargs)
|
||||||
self.server_version = self._connection.server_version
|
self._connection.autocommit = True
|
||||||
|
self.server_version = self._connection.server_version
|
||||||
return self._connection
|
return self._connection
|
||||||
|
|
||||||
def _cursor(self):
|
def _cursor(self):
|
||||||
@@ -435,6 +437,7 @@ class Postgresql(object):
|
|||||||
if ret:
|
if ret:
|
||||||
self.write_pg_hba(config.get('pg_hba', []))
|
self.write_pg_hba(config.get('pg_hba', []))
|
||||||
self._major_version = self.get_major_version()
|
self._major_version = self.get_major_version()
|
||||||
|
self._server_parameters = self.get_server_parameters(self.config)
|
||||||
else:
|
else:
|
||||||
self.set_state('initdb failed')
|
self.set_state('initdb failed')
|
||||||
return ret
|
return ret
|
||||||
@@ -697,9 +700,7 @@ class Postgresql(object):
|
|||||||
self._write_postgresql_conf()
|
self._write_postgresql_conf()
|
||||||
self.resolve_connection_addresses()
|
self.resolve_connection_addresses()
|
||||||
|
|
||||||
opts = {p: self._server_parameters[p] for p, v in self.CMDLINE_OPTIONS.items() if self._major_version >= v[2]}
|
opts = {p: self._server_parameters[p] for p in self.CMDLINE_OPTIONS if p in self._server_parameters}
|
||||||
if self._major_version >= 9.6 and opts['wal_level'] == 'hot_standby':
|
|
||||||
opts['wal_level'] = 'replica'
|
|
||||||
options = ['--{0}={1}'.format(p, v) for p, v in opts.items()]
|
options = ['--{0}={1}'.format(p, v) for p, v in opts.items()]
|
||||||
|
|
||||||
start_initiated = time.time()
|
start_initiated = time.time()
|
||||||
@@ -1180,9 +1181,12 @@ $$""".format(name, ' '.join(options)), name, password, password)
|
|||||||
|
|
||||||
# drop unused slots
|
# drop unused slots
|
||||||
for slot in set(self._replication_slots) - slots:
|
for slot in set(self._replication_slots) - slots:
|
||||||
self._query("""SELECT pg_drop_replication_slot(%s)
|
cursor = self._query("""SELECT pg_drop_replication_slot(%s)
|
||||||
WHERE EXISTS(SELECT 1 FROM pg_replication_slots
|
WHERE EXISTS(SELECT 1 FROM pg_replication_slots
|
||||||
WHERE slot_name = %s AND NOT active)""", slot, slot)
|
WHERE slot_name = %s AND NOT active)""", slot, slot)
|
||||||
|
|
||||||
|
if cursor.rowcount != 1: # Either slot doesn't exists or it is still active
|
||||||
|
self._schedule_load_slots = True # schedule load_replication_slots on the next iteration
|
||||||
|
|
||||||
# create new slots
|
# create new slots
|
||||||
for slot in slots - set(self._replication_slots):
|
for slot in slots - set(self._replication_slots):
|
||||||
@@ -1209,6 +1213,7 @@ $$""".format(name, ' '.join(options)), name, password, password)
|
|||||||
ret = self.create_replica(clone_member) == 0
|
ret = self.create_replica(clone_member) == 0
|
||||||
if ret:
|
if ret:
|
||||||
self._major_version = self.get_major_version()
|
self._major_version = self.get_major_version()
|
||||||
|
self._server_parameters = self.get_server_parameters(self.config)
|
||||||
self.delete_trigger_file()
|
self.delete_trigger_file()
|
||||||
self.restore_configuration_files()
|
self.restore_configuration_files()
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
+25
-24
@@ -6,13 +6,18 @@ from requests.exceptions import RequestException
|
|||||||
import sys
|
import sys
|
||||||
import boto.ec2
|
import boto.ec2
|
||||||
|
|
||||||
|
from patroni.utils import Retry, RetryFailedError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
retry_timeout = 15
|
||||||
|
|
||||||
|
|
||||||
class AWSConnection(object):
|
class AWSConnection(object):
|
||||||
def __init__(self, cluster_name):
|
def __init__(self, cluster_name):
|
||||||
self.available = False
|
self.available = False
|
||||||
self.cluster_name = cluster_name if cluster_name is not None else 'unknown'
|
self.cluster_name = cluster_name if cluster_name is not None else 'unknown'
|
||||||
|
self._retry = Retry(deadline=retry_timeout, max_delay=5, max_tries=-1, retry_exceptions=(boto.exception,))
|
||||||
try:
|
try:
|
||||||
# get the instance id
|
# get the instance id
|
||||||
r = requests.get('http://169.254.169.254/latest/dynamic/instance-identity/document', timeout=0.1)
|
r = requests.get('http://169.254.169.254/latest/dynamic/instance-identity/document', timeout=0.1)
|
||||||
@@ -29,40 +34,36 @@ class AWSConnection(object):
|
|||||||
return
|
return
|
||||||
self.available = True
|
self.available = True
|
||||||
|
|
||||||
|
def retry(self, *args, **kwargs):
|
||||||
|
return self._retry.copy()(*args, **kwargs)
|
||||||
|
|
||||||
def aws_available(self):
|
def aws_available(self):
|
||||||
return self.available
|
return self.available
|
||||||
|
|
||||||
def _tag_ebs(self, role):
|
def _tag_ebs(self, conn, role):
|
||||||
""" set tags, carrying the cluster name, instance role and instance id for the EBS storage """
|
""" set tags, carrying the cluster name, instance role and instance id for the EBS storage """
|
||||||
if not self.available:
|
|
||||||
return False
|
|
||||||
|
|
||||||
tags = {'Name': 'spilo_' + self.cluster_name, 'Role': role, 'Instance': self.instance_id}
|
tags = {'Name': 'spilo_' + self.cluster_name, 'Role': role, 'Instance': self.instance_id}
|
||||||
try:
|
volumes = conn.get_all_volumes(filters={'attachment.instance-id': self.instance_id})
|
||||||
conn = boto.ec2.connect_to_region(self.region)
|
conn.create_tags([v.id for v in volumes], tags)
|
||||||
volumes = conn.get_all_volumes(filters={'attachment.instance-id': self.instance_id})
|
|
||||||
conn.create_tags([v.id for v in volumes], tags)
|
|
||||||
except Exception as e:
|
|
||||||
logger.info('could not set tags for EBS storage devices attached: {}'.format(e))
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _tag_ec2(self, role):
|
def _tag_ec2(self, conn, role):
|
||||||
""" tag the current EC2 instance with a cluster role """
|
""" tag the current EC2 instance with a cluster role """
|
||||||
if not self.available:
|
|
||||||
return False
|
|
||||||
tags = {'Role': role}
|
tags = {'Role': role}
|
||||||
try:
|
conn.create_tags([self.instance_id], tags)
|
||||||
conn = boto.ec2.connect_to_region(self.region)
|
|
||||||
conn.create_tags([self.instance_id], tags)
|
|
||||||
except Exception as e:
|
|
||||||
logger.info("could not set tags for EC2 instance %s: %s", self.instance_id, e)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_role_change(self, new_role):
|
def on_role_change(self, new_role):
|
||||||
ret = self._tag_ec2(new_role)
|
if not self.available:
|
||||||
return self._tag_ebs(new_role) and ret
|
return False
|
||||||
|
try:
|
||||||
|
conn = self.retry(boto.ec2.connect_to_region, self.region)
|
||||||
|
self.retry(self._tag_ec2, conn, new_role)
|
||||||
|
self.retry(self._tag_ebs, conn, new_role)
|
||||||
|
except RetryFailedError:
|
||||||
|
logger.warning("Unable to communicate to AWS "
|
||||||
|
"when setting tags for the EC2 instance {0} "
|
||||||
|
"and attached EBS volumes".format(self.instance_id))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -133,8 +133,16 @@ class WALERestore(object):
|
|||||||
with psycopg2.connect(self.master_connection) as con:
|
with psycopg2.connect(self.master_connection) as con:
|
||||||
con.autocommit = True
|
con.autocommit = True
|
||||||
with con.cursor() as cur:
|
with con.cursor() as cur:
|
||||||
cur.execute("SELECT pg_xlog_location_diff(pg_current_xlog_location(), %s)",
|
cur.execute("""SELECT CASE WHEN pg_is_in_recovery()
|
||||||
(backup_start_lsn,))
|
THEN GREATEST(
|
||||||
|
pg_xlog_location_diff(COALESCE(
|
||||||
|
pg_last_xlog_receive_location(), '0/0'), %s)::bigint,
|
||||||
|
pg_xlog_location_diff(
|
||||||
|
pg_last_xlog_replay_location(), %s)::bigint)
|
||||||
|
ELSE pg_xlog_location_diff(
|
||||||
|
pg_current_xlog_location(), %s)::bigint
|
||||||
|
END""", (backup_start_lsn, backup_start_lsn, backup_start_lsn))
|
||||||
|
|
||||||
diff_in_bytes = int(cur.fetchone()[0])
|
diff_in_bytes = int(cur.fetchone()[0])
|
||||||
except psycopg2.Error:
|
except psycopg2.Error:
|
||||||
logger.exception('could not determine difference with the master location')
|
logger.exception('could not determine difference with the master location')
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
__version__ = '1.2.3'
|
__version__ = '1.2.5'
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ class MockRequest(object):
|
|||||||
def makefile(self, *args, **kwargs):
|
def makefile(self, *args, **kwargs):
|
||||||
return IO(self.request)
|
return IO(self.request)
|
||||||
|
|
||||||
|
def sendall(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
class MockRestApiServer(RestApiServer):
|
class MockRestApiServer(RestApiServer):
|
||||||
|
|
||||||
|
|||||||
+6
-15
@@ -6,6 +6,7 @@ import unittest
|
|||||||
from mock import Mock, patch
|
from mock import Mock, patch
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from patroni.scripts.aws import AWSConnection, main as _main
|
from patroni.scripts.aws import AWSConnection, main as _main
|
||||||
|
from patroni.utils import RetryFailedError
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
|
||||||
@@ -16,13 +17,13 @@ class MockEc2Connection(object):
|
|||||||
|
|
||||||
def get_all_volumes(self, filters):
|
def get_all_volumes(self, filters):
|
||||||
if self.error:
|
if self.error:
|
||||||
raise Exception("get_all_volumes")
|
raise boto.exception("get_all_volumes")
|
||||||
oid = namedtuple('Volume', 'id')
|
oid = namedtuple('Volume', 'id')
|
||||||
return [oid(id='a'), oid(id='b')]
|
return [oid(id='a'), oid(id='b')]
|
||||||
|
|
||||||
def create_tags(self, objects, tags):
|
def create_tags(self, objects, tags):
|
||||||
if self.error or len(objects) == 0:
|
if self.error or len(objects) == 0:
|
||||||
raise Exception("create_tags")
|
raise boto.exception("create_tags")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -63,30 +64,20 @@ class TestAWSConnection(unittest.TestCase):
|
|||||||
self.assertTrue(self.conn.aws_available())
|
self.assertTrue(self.conn.aws_available())
|
||||||
|
|
||||||
def test_on_role_change(self):
|
def test_on_role_change(self):
|
||||||
self.assertTrue(self.conn._tag_ebs('master'))
|
|
||||||
self.assertTrue(self.conn._tag_ec2('master'))
|
|
||||||
self.assertTrue(self.conn.on_role_change('master'))
|
self.assertTrue(self.conn.on_role_change('master'))
|
||||||
|
self.conn.retry = Mock(side_effect=RetryFailedError("retry failed"))
|
||||||
|
self.assertFalse(self.conn.on_role_change('master'))
|
||||||
|
|
||||||
def test_non_aws(self):
|
def test_non_aws(self):
|
||||||
self.error = True
|
self.error = True
|
||||||
conn = AWSConnection('test')
|
conn = AWSConnection('test')
|
||||||
self.assertFalse(conn.aws_available())
|
self.assertFalse(conn.on_role_change("master"))
|
||||||
self.assertFalse(conn._tag_ebs('master'))
|
|
||||||
self.assertFalse(conn._tag_ec2('master'))
|
|
||||||
|
|
||||||
def test_aws_bizare_response(self):
|
def test_aws_bizare_response(self):
|
||||||
self.json_error = True
|
self.json_error = True
|
||||||
conn = AWSConnection('test')
|
conn = AWSConnection('test')
|
||||||
self.assertFalse(conn.aws_available())
|
self.assertFalse(conn.aws_available())
|
||||||
|
|
||||||
def test_aws_tag_ebs_error(self):
|
|
||||||
self.error = True
|
|
||||||
self.assertFalse(self.conn._tag_ebs("master"))
|
|
||||||
|
|
||||||
def test_aws_tag_ec2_error(self):
|
|
||||||
self.error = True
|
|
||||||
self.assertFalse(self.conn._tag_ec2("master"))
|
|
||||||
|
|
||||||
@patch('sys.exit', Mock())
|
@patch('sys.exit', Mock())
|
||||||
def test_main(self):
|
def test_main(self):
|
||||||
self.assertIsNone(_main())
|
self.assertIsNone(_main())
|
||||||
|
|||||||
+6
-2
@@ -59,8 +59,10 @@ def etcd_watch(self, key, index=None, timeout=None, recursive=None):
|
|||||||
raise etcd.EtcdWatchTimedOut
|
raise etcd.EtcdWatchTimedOut
|
||||||
elif timeout == 5.0:
|
elif timeout == 5.0:
|
||||||
return etcd.EtcdResult('delete', {})
|
return etcd.EtcdResult('delete', {})
|
||||||
elif timeout == 10.0:
|
elif 5 < timeout <= 10.0:
|
||||||
raise etcd.EtcdException
|
raise etcd.EtcdException
|
||||||
|
elif timeout == 20.0:
|
||||||
|
raise etcd.EtcdEventIndexCleared
|
||||||
|
|
||||||
|
|
||||||
def etcd_write(self, key, value, **kwargs):
|
def etcd_write(self, key, value, **kwargs):
|
||||||
@@ -300,6 +302,7 @@ class TestEtcd(unittest.TestCase):
|
|||||||
def test_delete_cluster(self):
|
def test_delete_cluster(self):
|
||||||
self.assertFalse(self.etcd.delete_cluster())
|
self.assertFalse(self.etcd.delete_cluster())
|
||||||
|
|
||||||
|
@patch('time.sleep', Mock(side_effect=SleepException))
|
||||||
@patch.object(etcd.Client, 'watch', etcd_watch)
|
@patch.object(etcd.Client, 'watch', etcd_watch)
|
||||||
def test_watch(self):
|
def test_watch(self):
|
||||||
self.etcd.watch(None, 0)
|
self.etcd.watch(None, 0)
|
||||||
@@ -307,7 +310,8 @@ class TestEtcd(unittest.TestCase):
|
|||||||
self.etcd.watch(20729, 1.5)
|
self.etcd.watch(20729, 1.5)
|
||||||
self.etcd.watch(20729, 4.5)
|
self.etcd.watch(20729, 4.5)
|
||||||
with patch.object(AbstractDCS, 'watch', Mock()):
|
with patch.object(AbstractDCS, 'watch', Mock()):
|
||||||
self.etcd.watch(20729, 9.5)
|
self.assertTrue(self.etcd.watch(20729, 19.5))
|
||||||
|
self.assertRaises(SleepException, self.etcd.watch, 20729, 9.5)
|
||||||
|
|
||||||
def test_other_exceptions(self):
|
def test_other_exceptions(self):
|
||||||
self.etcd.retry = Mock(side_effect=AttributeError('foo'))
|
self.etcd.retry = Mock(side_effect=AttributeError('foo'))
|
||||||
|
|||||||
@@ -771,3 +771,14 @@ class TestHa(unittest.TestCase):
|
|||||||
|
|
||||||
def test_wakup(self):
|
def test_wakup(self):
|
||||||
self.ha.wakeup()
|
self.ha.wakeup()
|
||||||
|
|
||||||
|
def test_leader_with_empty_directory(self):
|
||||||
|
self.ha.cluster = get_cluster_initialized_with_leader()
|
||||||
|
self.ha.has_lock = true
|
||||||
|
self.p.data_directory_empty = true
|
||||||
|
self.assertEquals(self.ha.run_cycle(), 'released leader key voluntarily as data dir empty and currently leader')
|
||||||
|
|
||||||
|
# as has_lock is mocked out, we need to fake the leader key release
|
||||||
|
self.ha.has_lock = false
|
||||||
|
# will not say bootstrap from leader as replica can't self elect
|
||||||
|
self.assertEquals(self.ha.run_cycle(), "trying to bootstrap from replica 'other'")
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class MockCursor(object):
|
|||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
self.closed = False
|
self.closed = False
|
||||||
|
self.rowcount = 0
|
||||||
self.results = []
|
self.results = []
|
||||||
|
|
||||||
def execute(self, sql, *params):
|
def execute(self, sql, *params):
|
||||||
@@ -41,8 +42,7 @@ class MockCursor(object):
|
|||||||
('search_path', 'public', None, 'string', 'user'),
|
('search_path', 'public', None, 'string', 'user'),
|
||||||
('port', '5433', None, 'integer', 'postmaster'),
|
('port', '5433', None, 'integer', 'postmaster'),
|
||||||
('listen_addresses', '*', None, 'string', 'postmaster'),
|
('listen_addresses', '*', None, 'string', 'postmaster'),
|
||||||
('autovacuum', 'on', None, 'bool', 'sighup'),
|
('autovacuum', 'on', None, 'bool', 'sighup')]
|
||||||
('wal_level', 'replica', None, 'enum', 'postmaster')]
|
|
||||||
else:
|
else:
|
||||||
self.results = [(None, None, None, None, None, None, None, None, None, None)]
|
self.results = [(None, None, None, None, None, None, None, None, None, None)]
|
||||||
|
|
||||||
@@ -766,7 +766,7 @@ class TestPostgresql(unittest.TestCase):
|
|||||||
self.assertEquals(value_in_conf(), None)
|
self.assertEquals(value_in_conf(), None)
|
||||||
|
|
||||||
def test_get_server_parameters(self):
|
def test_get_server_parameters(self):
|
||||||
config = {'synchronous_mode': True, 'parameters': {}, 'listen': '0'}
|
config = {'synchronous_mode': True, 'parameters': {'wal_level': 'hot_standby'}, 'listen': '0'}
|
||||||
self.p.get_server_parameters(config)
|
self.p.get_server_parameters(config)
|
||||||
self.p.set_synchronous_standby('foo')
|
self.p.set_synchronous_standby('foo')
|
||||||
self.p.get_server_parameters(config)
|
self.p.get_server_parameters(config)
|
||||||
|
|||||||
Reference in New Issue
Block a user