Merge branches watch-leader-key and package-refactoring

This commit is contained in:
Alexander Kukushkin
2015-09-05 16:09:28 +02:00
25 changed files with 180 additions and 161 deletions
+1 -121
View File
@@ -1,125 +1,5 @@
#!/usr/bin/env python
import logging
import os
import sys
import time
import yaml
from helpers.api import RestApiServer
from helpers.etcd import Etcd
from helpers.ha import Ha
from helpers.postgresql import Postgresql
from helpers.utils import setup_signal_handlers, sleep, reap_children
from helpers.zookeeper import ZooKeeper
logger = logging.getLogger(__name__)
class Patroni:
def __init__(self, config):
self.nap_time = config['loop_wait']
self.postgresql = Postgresql(config['postgresql'])
self.ha = Ha(self.postgresql, self.get_dcs(self.postgresql.name, config))
host, port = config['restapi']['listen'].split(':')
self.api = RestApiServer(self, config['restapi'])
self.next_run = time.time()
self.shutdown_member_ttl = 300
@staticmethod
def get_dcs(name, config):
if 'etcd' in config:
return Etcd(name, config['etcd'])
if 'zookeeper' in config:
return ZooKeeper(name, config['zookeeper'])
raise Exception('Can not find sutable configuration of distributed configuration store')
def touch_member(self, ttl=None):
connection_string = self.postgresql.connection_string + '?application_name=' + self.api.connection_string
if self.ha.cluster:
for m in self.ha.cluster.members:
# Do not update member TTL when it is far from being expired
if m.name == self.postgresql.name and m.real_ttl() > self.shutdown_member_ttl:
return True
return self.ha.dcs.touch_member(connection_string, ttl)
def initialize(self):
# wait for etcd to be available
while not self.touch_member():
logger.info('waiting on DCS')
sleep(5)
# is data directory empty?
if self.postgresql.data_directory_empty():
# racing to initialize
if self.ha.dcs.race('/initialize'):
self.postgresql.initialize()
self.ha.dcs.take_leader()
self.postgresql.start()
self.postgresql.create_replication_user()
self.postgresql.create_connection_users()
else:
while True:
leader = self.ha.dcs.current_leader()
if leader and self.postgresql.sync_from_leader(leader):
self.postgresql.write_recovery_conf(leader)
self.postgresql.start()
break
sleep(5)
elif self.postgresql.is_running():
self.postgresql.load_replication_slots()
def schedule_next_run(self):
if self.postgresql.is_promoted:
self.next_run = time.time()
self.next_run += self.nap_time
current_time = time.time()
nap_time = self.next_run - current_time
if nap_time <= 0:
self.next_run = current_time
else:
self.ha.dcs.watch(nap_time)
def run(self):
self.api.start()
self.next_run = time.time()
while True:
self.touch_member()
logger.info(self.ha.run_cycle())
try:
if self.ha.state_handler.is_leader():
self.ha.cluster and self.ha.state_handler.create_replication_slots(self.ha.cluster)
else:
self.ha.state_handler.drop_replication_slots()
except:
logger.exception('Exception when changing replication slots')
reap_children()
self.schedule_next_run()
def main():
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
logging.getLogger('requests').setLevel(logging.WARNING)
setup_signal_handlers()
if len(sys.argv) < 2 or not os.path.isfile(sys.argv[1]):
print('Usage: {} config.yml'.format(sys.argv[0]))
return
with open(sys.argv[1], 'r') as f:
config = yaml.load(f)
patroni = Patroni(config)
try:
patroni.initialize()
patroni.run()
except KeyboardInterrupt:
pass
finally:
patroni.touch_member(patroni.shutdown_member_ttl) # schedule member removal
patroni.postgresql.stop()
patroni.ha.dcs.delete_leader()
from patroni import main
if __name__ == '__main__':
+121
View File
@@ -0,0 +1,121 @@
import logging
import os
import sys
import time
import yaml
from patroni.api import RestApiServer
from patroni.etcd import Etcd
from patroni.ha import Ha
from patroni.postgresql import Postgresql
from patroni.utils import setup_signal_handlers, sleep, reap_children
from patroni.zookeeper import ZooKeeper
logger = logging.getLogger(__name__)
class Patroni:
def __init__(self, config):
self.nap_time = config['loop_wait']
self.postgresql = Postgresql(config['postgresql'])
self.ha = Ha(self.postgresql, self.get_dcs(self.postgresql.name, config))
host, port = config['restapi']['listen'].split(':')
self.api = RestApiServer(self, config['restapi'])
self.next_run = time.time()
self.shutdown_member_ttl = 300
@staticmethod
def get_dcs(name, config):
if 'etcd' in config:
return Etcd(name, config['etcd'])
if 'zookeeper' in config:
return ZooKeeper(name, config['zookeeper'])
raise Exception('Can not find sutable configuration of distributed configuration store')
def touch_member(self, ttl=None):
connection_string = self.postgresql.connection_string + '?application_name=' + self.api.connection_string
if self.ha.cluster:
for m in self.ha.cluster.members:
# Do not update member TTL when it is far from being expired
if m.name == self.postgresql.name and m.real_ttl() > self.shutdown_member_ttl:
return True
return self.ha.dcs.touch_member(connection_string, ttl)
def initialize(self):
# wait for etcd to be available
while not self.touch_member():
logger.info('waiting on DCS')
sleep(5)
# is data directory empty?
if self.postgresql.data_directory_empty():
# racing to initialize
if self.ha.dcs.race('/initialize'):
self.postgresql.initialize()
self.ha.dcs.take_leader()
self.postgresql.start()
self.postgresql.create_replication_user()
self.postgresql.create_connection_users()
else:
while True:
leader = self.ha.dcs.current_leader()
if leader and self.postgresql.sync_from_leader(leader):
self.postgresql.write_recovery_conf(leader)
self.postgresql.start()
break
sleep(5)
elif self.postgresql.is_running():
self.postgresql.load_replication_slots()
def schedule_next_run(self):
if self.postgresql.is_promoted:
self.next_run = time.time()
self.next_run += self.nap_time
current_time = time.time()
nap_time = self.next_run - current_time
if nap_time <= 0:
self.next_run = current_time
else:
self.ha.dcs.watch(nap_time)
def run(self):
self.api.start()
self.next_run = time.time()
while True:
self.touch_member()
logger.info(self.ha.run_cycle())
try:
if self.ha.state_handler.is_leader():
self.ha.cluster and self.ha.state_handler.create_replication_slots(self.ha.cluster)
else:
self.ha.state_handler.drop_replication_slots()
except:
logger.exception('Exception when changing replication slots')
reap_children()
self.schedule_next_run()
def main():
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
logging.getLogger('requests').setLevel(logging.WARNING)
setup_signal_handlers()
if len(sys.argv) < 2 or not os.path.isfile(sys.argv[1]):
print('Usage: {} config.yml'.format(sys.argv[0]))
return
with open(sys.argv[1], 'r') as f:
config = yaml.load(f)
patroni = Patroni(config)
try:
patroni.initialize()
patroni.run()
except KeyboardInterrupt:
pass
finally:
patroni.touch_member(patroni.shutdown_member_ttl) # schedule member removal
patroni.postgresql.stop()
patroni.ha.dcs.delete_leader()
+5
View File
@@ -0,0 +1,5 @@
from patroni import main
if __name__ == '__main__':
main()
View File
+5 -2
View File
@@ -1,8 +1,8 @@
import abc
from collections import namedtuple
from helpers import DCSError
from helpers.utils import calculate_ttl, sleep
from patroni.exceptions import DCSError
from patroni.utils import calculate_ttl, sleep
from six.moves.urllib_parse import urlparse, urlunparse, parse_qsl
@@ -24,6 +24,7 @@ def parse_connection_string(value):
class Member(namedtuple('Member', 'index,name,conn_url,api_url,expiration,ttl')):
"""Immutable object (namedtuple) which represents single member of PostgreSQL cluster.
Consists of the following fields:
:param index: modification index of a given member key in a Configuration Store
@@ -38,6 +39,7 @@ class Member(namedtuple('Member', 'index,name,conn_url,api_url,expiration,ttl'))
class Leader(namedtuple('Leader', 'index,expiration,ttl,member')):
"""Immutable object (namedtuple) which represents leader key.
Consists of the following fields:
:param index: modification index of a leader key in a Configuration Store
@@ -55,6 +57,7 @@ class Leader(namedtuple('Leader', 'index,expiration,ttl,member')):
class Cluster(namedtuple('Cluster', 'initialize,leader,last_leader_operation,members')):
"""Immutable object (namedtuple) which represents PostgreSQL cluster.
Consists of the following fields:
:param initialize: boolean, shows whether this cluster has initialization key stored in DC or not.
+2 -2
View File
@@ -10,8 +10,8 @@ import urllib3
from dns.exception import DNSException
from dns import resolver
from helpers.dcs import AbstractDCS, Cluster, DCSError, Leader, Member, parse_connection_string
from helpers.utils import Retry, RetryFailedError, sleep
from patroni.dcs import AbstractDCS, Cluster, DCSError, Leader, Member, parse_connection_string
from patroni.utils import Retry, RetryFailedError, sleep
from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
@@ -3,6 +3,7 @@ class PatroniException(Exception):
class DCSError(PatroniException):
"""Parent class for all kind of exceptions related to selected distributed configuration store"""
def __init__(self, value):
+1 -1
View File
@@ -1,6 +1,6 @@
import logging
from helpers.dcs import DCSError
from patroni.dcs import DCSError
from psycopg2 import InterfaceError, OperationalError
logger = logging.getLogger(__name__)
@@ -5,7 +5,7 @@ import shlex
import shutil
import subprocess
from helpers.utils import sleep
from patroni.utils import sleep
from six.moves.urllib_parse import urlparse
logger = logging.getLogger(__name__)
+3 -1
View File
@@ -6,7 +6,7 @@ import signal
import sys
import time
from helpers import DCSError
from patroni.exceptions import DCSError
interrupted_sleep = False
reap_children = False
@@ -87,10 +87,12 @@ def reap_children():
class RetryFailedError(DCSError):
"""Raised when retrying an operation ultimately failed, after retrying the maximum number of attempts."""
class Retry:
"""Helper for retrying a method in the face of retry-able exceptions"""
def __init__(self, max_tries=1, delay=0.1, backoff=2, max_jitter=0.8, max_delay=3600,
+1
View File
@@ -0,0 +1 @@
__version__ = '0.1'
@@ -3,10 +3,10 @@ import random
import requests
import time
from helpers.dcs import AbstractDCS, Cluster, DCSError, Leader, Member, parse_connection_string
from helpers.utils import sleep
from kazoo.client import KazooClient, KazooState
from kazoo.exceptions import NoNodeError, NodeExistsError
from patroni.dcs import AbstractDCS, Cluster, DCSError, Leader, Member, parse_connection_string
from patroni.utils import sleep
from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
+12 -6
View File
@@ -19,13 +19,20 @@ if sys.version_info < (2, 7, 0):
__location__ = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())))
def read_version(package):
data = {}
with open(os.path.join(package, 'version.py'), 'r') as fd:
exec(fd.read(), data)
return data['__version__']
NAME = 'patroni'
MAIN_PACKAGE = 'patroni.py'
HELPERS = 'helpers'
MAIN_PACKAGE = NAME
SCRIPTS = 'scripts'
VERSION = read_version(MAIN_PACKAGE)
VERSION = '0.1'
DESCRIPTION = 'A Template for PostgreSQL HA with etcd'
LICENSE = 'The MIT License'
LICENSE = 'MIT License'
COVERAGE_XML = True
COVERAGE_HTML = False
@@ -62,8 +69,7 @@ class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
if self.cov_xml or self.cov_html:
self.cov = ['--cov', MAIN_PACKAGE, '--cov', HELPERS, '--cov', SCRIPTS, '--cov-report',
'term-missing']
self.cov = ['--cov', MAIN_PACKAGE, '--cov', MAIN_PACKAGE, '--cov-report', 'term-missing']
if self.cov_xml:
self.cov.extend(['--cov-report', 'xml'])
if self.cov_html:
@@ -82,7 +88,7 @@ class PyTest(TestCommand):
params['plugins'] = ['cov']
if self.junitxml:
params['args'] += self.junitxml
params['args'] += ['--doctest-modules', HELPERS, '--doctest-modules', SCRIPTS, '-s']
params['args'] += ['--doctest-modules', MAIN_PACKAGE, '-s', '-vv']
errno = pytest.main(**params)
sys.exit(errno)
+1 -1
View File
@@ -1,7 +1,7 @@
import psycopg2
import unittest
from helpers.api import RestApiHandler, RestApiServer
from patroni.api import RestApiHandler, RestApiServer
from six import BytesIO as IO
from test_postgresql import psycopg2_connect
+1 -1
View File
@@ -2,7 +2,7 @@ import unittest
import requests
import boto.ec2
from collections import namedtuple
from scripts.aws import AWSConnection
from patroni.scripts.aws import AWSConnection
from requests.exceptions import RequestException
+2 -2
View File
@@ -9,9 +9,9 @@ import time
import unittest
from dns.exception import DNSException
from helpers.dcs import Cluster, DCSError, Leader, Member
from helpers.etcd import Client, Etcd
from mock import Mock, patch
from patroni.dcs import Cluster, DCSError, Leader, Member
from patroni.etcd import Client, Etcd
class MockResponse:
+3 -3
View File
@@ -1,9 +1,9 @@
import unittest
from helpers.dcs import Cluster, DCSError
from helpers.etcd import Client, Etcd
from helpers.ha import Ha
from mock import Mock, patch
from patroni.dcs import Cluster, DCSError
from patroni.etcd import Client, Etcd
from patroni.ha import Ha
from test_etcd import etcd_read, etcd_write
+7 -7
View File
@@ -1,5 +1,5 @@
import datetime
import helpers.zookeeper
import patroni.zookeeper
import psycopg2
import subprocess
import sys
@@ -7,14 +7,14 @@ import time
import unittest
import yaml
from helpers.api import RestApiServer
from helpers.dcs import Cluster, Member
from helpers.etcd import Etcd
from helpers.zookeeper import ZooKeeper
from mock import Mock, patch
from patroni.api import RestApiServer
from patroni.dcs import Cluster, Member
from patroni.etcd import Etcd
from patroni import Patroni, main
from patroni.zookeeper import ZooKeeper
from six.moves import BaseHTTPServer
from test_etcd import Client, etcd_read, etcd_write, etcd_watch
from test_etcd import Client, etcd_read, etcd_write
from test_ha import true, false
from test_postgresql import Postgresql, subprocess_call, psycopg2_connect
from test_zookeeper import MockKazooClient
@@ -74,7 +74,7 @@ class TestPatroni(unittest.TestCase):
Postgresql.write_recovery_conf = self.write_recovery_conf
def test_get_dcs(self):
helpers.zookeeper.KazooClient = MockKazooClient
patroni.zookeeper.KazooClient = MockKazooClient
self.assertIsInstance(self.p.get_dcs('', {'zookeeper': {'scope': '', 'hosts': ''}}), ZooKeeper)
self.assertRaises(Exception, self.p.get_dcs, '', {})
+2 -2
View File
@@ -4,8 +4,8 @@ import shutil
import subprocess
import unittest
from helpers.dcs import Cluster, Leader, Member
from helpers.postgresql import Postgresql
from patroni.dcs import Cluster, Leader, Member
from patroni.postgresql import Postgresql
def nop(*args, **kwargs):
+1 -1
View File
@@ -1,7 +1,7 @@
import unittest
from mock import MagicMock, patch
import os
from scripts.restore import Restore, WALERestore
from patroni.scripts.restore import Restore, WALERestore
def fake_cursor_fetchone(*args, **kwargs):
+3 -3
View File
@@ -2,8 +2,8 @@ import os
import time
import unittest
from helpers import DCSError
from helpers.utils import Retry, RetryFailedError, reap_children, sigchld_handler, sigterm_handler, sleep
from patroni.exceptions import DCSError
from patroni.utils import Retry, RetryFailedError, reap_children, sigchld_handler, sigterm_handler, sleep
def nop(*args, **kwargs):
@@ -92,7 +92,7 @@ class TestRetrySleeper(unittest.TestCase):
pass
retry = self._makeOne(deadline=0.0001, sleep_func=sleep_func)
self.assertRaises(RetryFailedError, retry, self._fail(times=10))
self.assertRaises(RetryFailedError, retry, self._fail(times=100))
def test_copy(self):
_sleep = lambda t: None
+5 -5
View File
@@ -1,9 +1,9 @@
import helpers.zookeeper
import patroni.zookeeper
import requests
import unittest
from helpers.dcs import Leader
from helpers.zookeeper import ExhibitorEnsembleProvider, ZooKeeper, ZooKeeperError
from patroni.dcs import Leader
from patroni.zookeeper import ExhibitorEnsembleProvider, ZooKeeper, ZooKeeperError
from kazoo.client import KazooState
from kazoo.exceptions import NoNodeError, NodeExistsError
from kazoo.protocol.states import ZnodeStat
@@ -110,7 +110,7 @@ class TestExhibitorEnsembleProvider(unittest.TestCase):
def set_up(self):
requests.get = requests_get
helpers.zookeeper.sleep = exhibitor_sleep
patroni.zookeeper.sleep = exhibitor_sleep
def test_init(self):
self.assertRaises(SleepException, ExhibitorEnsembleProvider, ['localhost'], 8181)
@@ -124,7 +124,7 @@ class TestZooKeeper(unittest.TestCase):
def set_up(self):
requests.get = requests_get
helpers.zookeeper.KazooClient = MockKazooClient
patroni.zookeeper.KazooClient = MockKazooClient
self.zk = ZooKeeper('foo', {'exhibitor': {'hosts': ['localhost', 'exhibitor'], 'port': 8181}, 'scope': 'test'})
def test_session_listener(self):