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 #!/usr/bin/env python
import logging from patroni import main
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()
if __name__ == '__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 import abc
from collections import namedtuple from collections import namedtuple
from helpers import DCSError from patroni.exceptions import DCSError
from helpers.utils import calculate_ttl, sleep from patroni.utils import calculate_ttl, sleep
from six.moves.urllib_parse import urlparse, urlunparse, parse_qsl 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')): class Member(namedtuple('Member', 'index,name,conn_url,api_url,expiration,ttl')):
"""Immutable object (namedtuple) which represents single member of PostgreSQL cluster. """Immutable object (namedtuple) which represents single member of PostgreSQL cluster.
Consists of the following fields: Consists of the following fields:
:param index: modification index of a given member key in a Configuration Store :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')): class Leader(namedtuple('Leader', 'index,expiration,ttl,member')):
"""Immutable object (namedtuple) which represents leader key. """Immutable object (namedtuple) which represents leader key.
Consists of the following fields: Consists of the following fields:
:param index: modification index of a leader key in a Configuration Store :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')): class Cluster(namedtuple('Cluster', 'initialize,leader,last_leader_operation,members')):
"""Immutable object (namedtuple) which represents PostgreSQL cluster. """Immutable object (namedtuple) which represents PostgreSQL cluster.
Consists of the following fields: Consists of the following fields:
:param initialize: boolean, shows whether this cluster has initialization key stored in DC or not. :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.exception import DNSException
from dns import resolver from dns import resolver
from helpers.dcs import AbstractDCS, Cluster, DCSError, Leader, Member, parse_connection_string from patroni.dcs import AbstractDCS, Cluster, DCSError, Leader, Member, parse_connection_string
from helpers.utils import Retry, RetryFailedError, sleep from patroni.utils import Retry, RetryFailedError, sleep
from requests.exceptions import RequestException from requests.exceptions import RequestException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -3,6 +3,7 @@ class PatroniException(Exception):
class DCSError(PatroniException): class DCSError(PatroniException):
"""Parent class for all kind of exceptions related to selected distributed configuration store""" """Parent class for all kind of exceptions related to selected distributed configuration store"""
def __init__(self, value): def __init__(self, value):
+1 -1
View File
@@ -1,6 +1,6 @@
import logging import logging
from helpers.dcs import DCSError from patroni.dcs import DCSError
from psycopg2 import InterfaceError, OperationalError from psycopg2 import InterfaceError, OperationalError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -5,7 +5,7 @@ import shlex
import shutil import shutil
import subprocess import subprocess
from helpers.utils import sleep from patroni.utils import sleep
from six.moves.urllib_parse import urlparse from six.moves.urllib_parse import urlparse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+3 -1
View File
@@ -6,7 +6,7 @@ import signal
import sys import sys
import time import time
from helpers import DCSError from patroni.exceptions import DCSError
interrupted_sleep = False interrupted_sleep = False
reap_children = False reap_children = False
@@ -87,10 +87,12 @@ def reap_children():
class RetryFailedError(DCSError): class RetryFailedError(DCSError):
"""Raised when retrying an operation ultimately failed, after retrying the maximum number of attempts.""" """Raised when retrying an operation ultimately failed, after retrying the maximum number of attempts."""
class Retry: class Retry:
"""Helper for retrying a method in the face of retry-able exceptions""" """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, 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 requests
import time 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.client import KazooClient, KazooState
from kazoo.exceptions import NoNodeError, NodeExistsError 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 from requests.exceptions import RequestException
logger = logging.getLogger(__name__) 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()))) __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' NAME = 'patroni'
MAIN_PACKAGE = 'patroni.py' MAIN_PACKAGE = NAME
HELPERS = 'helpers'
SCRIPTS = 'scripts' SCRIPTS = 'scripts'
VERSION = read_version(MAIN_PACKAGE)
VERSION = '0.1' VERSION = '0.1'
DESCRIPTION = 'A Template for PostgreSQL HA with etcd' DESCRIPTION = 'A Template for PostgreSQL HA with etcd'
LICENSE = 'The MIT License' LICENSE = 'MIT License'
COVERAGE_XML = True COVERAGE_XML = True
COVERAGE_HTML = False COVERAGE_HTML = False
@@ -62,8 +69,7 @@ class PyTest(TestCommand):
def finalize_options(self): def finalize_options(self):
TestCommand.finalize_options(self) TestCommand.finalize_options(self)
if self.cov_xml or self.cov_html: if self.cov_xml or self.cov_html:
self.cov = ['--cov', MAIN_PACKAGE, '--cov', HELPERS, '--cov', SCRIPTS, '--cov-report', self.cov = ['--cov', MAIN_PACKAGE, '--cov', MAIN_PACKAGE, '--cov-report', 'term-missing']
'term-missing']
if self.cov_xml: if self.cov_xml:
self.cov.extend(['--cov-report', 'xml']) self.cov.extend(['--cov-report', 'xml'])
if self.cov_html: if self.cov_html:
@@ -82,7 +88,7 @@ class PyTest(TestCommand):
params['plugins'] = ['cov'] params['plugins'] = ['cov']
if self.junitxml: if self.junitxml:
params['args'] += 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) errno = pytest.main(**params)
sys.exit(errno) sys.exit(errno)
+1 -1
View File
@@ -1,7 +1,7 @@
import psycopg2 import psycopg2
import unittest import unittest
from helpers.api import RestApiHandler, RestApiServer from patroni.api import RestApiHandler, RestApiServer
from six import BytesIO as IO from six import BytesIO as IO
from test_postgresql import psycopg2_connect from test_postgresql import psycopg2_connect
+1 -1
View File
@@ -2,7 +2,7 @@ import unittest
import requests import requests
import boto.ec2 import boto.ec2
from collections import namedtuple from collections import namedtuple
from scripts.aws import AWSConnection from patroni.scripts.aws import AWSConnection
from requests.exceptions import RequestException from requests.exceptions import RequestException
+2 -2
View File
@@ -9,9 +9,9 @@ import time
import unittest import unittest
from dns.exception import DNSException 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 mock import Mock, patch
from patroni.dcs import Cluster, DCSError, Leader, Member
from patroni.etcd import Client, Etcd
class MockResponse: class MockResponse:
+3 -3
View File
@@ -1,9 +1,9 @@
import unittest 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 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 from test_etcd import etcd_read, etcd_write
+7 -7
View File
@@ -1,5 +1,5 @@
import datetime import datetime
import helpers.zookeeper import patroni.zookeeper
import psycopg2 import psycopg2
import subprocess import subprocess
import sys import sys
@@ -7,14 +7,14 @@ import time
import unittest import unittest
import yaml 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 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 import Patroni, main
from patroni.zookeeper import ZooKeeper
from six.moves import BaseHTTPServer 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_ha import true, false
from test_postgresql import Postgresql, subprocess_call, psycopg2_connect from test_postgresql import Postgresql, subprocess_call, psycopg2_connect
from test_zookeeper import MockKazooClient from test_zookeeper import MockKazooClient
@@ -74,7 +74,7 @@ class TestPatroni(unittest.TestCase):
Postgresql.write_recovery_conf = self.write_recovery_conf Postgresql.write_recovery_conf = self.write_recovery_conf
def test_get_dcs(self): def test_get_dcs(self):
helpers.zookeeper.KazooClient = MockKazooClient patroni.zookeeper.KazooClient = MockKazooClient
self.assertIsInstance(self.p.get_dcs('', {'zookeeper': {'scope': '', 'hosts': ''}}), ZooKeeper) self.assertIsInstance(self.p.get_dcs('', {'zookeeper': {'scope': '', 'hosts': ''}}), ZooKeeper)
self.assertRaises(Exception, self.p.get_dcs, '', {}) self.assertRaises(Exception, self.p.get_dcs, '', {})
+2 -2
View File
@@ -4,8 +4,8 @@ import shutil
import subprocess import subprocess
import unittest import unittest
from helpers.dcs import Cluster, Leader, Member from patroni.dcs import Cluster, Leader, Member
from helpers.postgresql import Postgresql from patroni.postgresql import Postgresql
def nop(*args, **kwargs): def nop(*args, **kwargs):
+1 -1
View File
@@ -1,7 +1,7 @@
import unittest import unittest
from mock import MagicMock, patch from mock import MagicMock, patch
import os import os
from scripts.restore import Restore, WALERestore from patroni.scripts.restore import Restore, WALERestore
def fake_cursor_fetchone(*args, **kwargs): def fake_cursor_fetchone(*args, **kwargs):
+3 -3
View File
@@ -2,8 +2,8 @@ import os
import time import time
import unittest import unittest
from helpers import DCSError from patroni.exceptions import DCSError
from helpers.utils import Retry, RetryFailedError, reap_children, sigchld_handler, sigterm_handler, sleep from patroni.utils import Retry, RetryFailedError, reap_children, sigchld_handler, sigterm_handler, sleep
def nop(*args, **kwargs): def nop(*args, **kwargs):
@@ -92,7 +92,7 @@ class TestRetrySleeper(unittest.TestCase):
pass pass
retry = self._makeOne(deadline=0.0001, sleep_func=sleep_func) 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): def test_copy(self):
_sleep = lambda t: None _sleep = lambda t: None
+5 -5
View File
@@ -1,9 +1,9 @@
import helpers.zookeeper import patroni.zookeeper
import requests import requests
import unittest import unittest
from helpers.dcs import Leader from patroni.dcs import Leader
from helpers.zookeeper import ExhibitorEnsembleProvider, ZooKeeper, ZooKeeperError from patroni.zookeeper import ExhibitorEnsembleProvider, ZooKeeper, ZooKeeperError
from kazoo.client import KazooState from kazoo.client import KazooState
from kazoo.exceptions import NoNodeError, NodeExistsError from kazoo.exceptions import NoNodeError, NodeExistsError
from kazoo.protocol.states import ZnodeStat from kazoo.protocol.states import ZnodeStat
@@ -110,7 +110,7 @@ class TestExhibitorEnsembleProvider(unittest.TestCase):
def set_up(self): def set_up(self):
requests.get = requests_get requests.get = requests_get
helpers.zookeeper.sleep = exhibitor_sleep patroni.zookeeper.sleep = exhibitor_sleep
def test_init(self): def test_init(self):
self.assertRaises(SleepException, ExhibitorEnsembleProvider, ['localhost'], 8181) self.assertRaises(SleepException, ExhibitorEnsembleProvider, ['localhost'], 8181)
@@ -124,7 +124,7 @@ class TestZooKeeper(unittest.TestCase):
def set_up(self): def set_up(self):
requests.get = requests_get requests.get = requests_get
helpers.zookeeper.KazooClient = MockKazooClient patroni.zookeeper.KazooClient = MockKazooClient
self.zk = ZooKeeper('foo', {'exhibitor': {'hosts': ['localhost', 'exhibitor'], 'port': 8181}, 'scope': 'test'}) self.zk = ZooKeeper('foo', {'exhibitor': {'hosts': ['localhost', 'exhibitor'], 'port': 8181}, 'scope': 'test'})
def test_session_listener(self): def test_session_listener(self):