mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-25 14:53:37 +00:00
Solve issue of handling sigchld when dunning in a docker (#355)
If Patroni was started in a docker with pid=1 it will execute itself with the same arguments. The original process will take care about init process duties, i.e. handle sigchld and reap dead orphan processes. Also it will forward SIGINT, SIGHUP, SIGTERM and some other signals to the real Patroni process.
This commit is contained in:
committed by
GitHub
parent
038b5aed72
commit
28b00dea16
+46
-14
@@ -1,23 +1,22 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from patroni.api import RestApiServer
|
|
||||||
from patroni.config import Config
|
|
||||||
from patroni.dcs import get_dcs
|
|
||||||
from patroni.exceptions import DCSError
|
|
||||||
from patroni.ha import Ha
|
|
||||||
from patroni.postgresql import Postgresql
|
|
||||||
from patroni.utils import reap_children, sigchld_handler
|
|
||||||
from patroni.version import __version__
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Patroni(object):
|
class Patroni(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
from patroni.api import RestApiServer
|
||||||
|
from patroni.config import Config
|
||||||
|
from patroni.dcs import get_dcs
|
||||||
|
from patroni.ha import Ha
|
||||||
|
from patroni.postgresql import Postgresql
|
||||||
|
from patroni.version import __version__
|
||||||
|
|
||||||
self.setup_signal_handlers()
|
self.setup_signal_handlers()
|
||||||
|
|
||||||
self.version = __version__
|
self.version = __version__
|
||||||
@@ -34,6 +33,7 @@ class Patroni(object):
|
|||||||
self.scheduled_restart = {}
|
self.scheduled_restart = {}
|
||||||
|
|
||||||
def load_dynamic_configuration(self):
|
def load_dynamic_configuration(self):
|
||||||
|
from patroni.exceptions import DCSError
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
cluster = self.dcs.get_cluster()
|
cluster = self.dcs.get_cluster()
|
||||||
@@ -107,8 +107,6 @@ class Patroni(object):
|
|||||||
if self.config.reload_local_configuration():
|
if self.config.reload_local_configuration():
|
||||||
self.reload_config()
|
self.reload_config()
|
||||||
|
|
||||||
reap_children()
|
|
||||||
|
|
||||||
logger.info(self.ha.run_cycle())
|
logger.info(self.ha.run_cycle())
|
||||||
|
|
||||||
cluster = self.dcs.cluster
|
cluster = self.dcs.cluster
|
||||||
@@ -118,7 +116,6 @@ class Patroni(object):
|
|||||||
if not self.postgresql.data_directory_empty():
|
if not self.postgresql.data_directory_empty():
|
||||||
self.config.save_cache()
|
self.config.save_cache()
|
||||||
|
|
||||||
reap_children()
|
|
||||||
self.schedule_next_run()
|
self.schedule_next_run()
|
||||||
|
|
||||||
def setup_signal_handlers(self):
|
def setup_signal_handlers(self):
|
||||||
@@ -126,10 +123,9 @@ class Patroni(object):
|
|||||||
self._received_sigterm = False
|
self._received_sigterm = False
|
||||||
signal.signal(signal.SIGHUP, self.sighup_handler)
|
signal.signal(signal.SIGHUP, self.sighup_handler)
|
||||||
signal.signal(signal.SIGTERM, self.sigterm_handler)
|
signal.signal(signal.SIGTERM, self.sigterm_handler)
|
||||||
signal.signal(signal.SIGCHLD, sigchld_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def patroni_main():
|
||||||
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
|
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
|
||||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||||
|
|
||||||
@@ -145,3 +141,39 @@ def main():
|
|||||||
else:
|
else:
|
||||||
patroni.ha.while_not_sync_standby(lambda: patroni.postgresql.stop(checkpoint=False))
|
patroni.ha.while_not_sync_standby(lambda: patroni.postgresql.stop(checkpoint=False))
|
||||||
patroni.dcs.delete_leader()
|
patroni.dcs.delete_leader()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if os.getpid() != 1:
|
||||||
|
return patroni_main()
|
||||||
|
|
||||||
|
pid = 0
|
||||||
|
|
||||||
|
# Looks like we are in a docker, so we will act like init
|
||||||
|
def sigchld_handler(signo, stack_frame):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
ret = os.waitpid(-1, os.WNOHANG)
|
||||||
|
if ret == (0, 0):
|
||||||
|
break
|
||||||
|
elif ret[0] != pid:
|
||||||
|
logging.info('Reaped pid=%s, exit status=%s', *ret)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def passtochild(signo, stack_frame):
|
||||||
|
if pid:
|
||||||
|
os.kill(pid, signo)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGCHLD, sigchld_handler)
|
||||||
|
signal.signal(signal.SIGHUP, passtochild)
|
||||||
|
signal.signal(signal.SIGINT, passtochild)
|
||||||
|
signal.signal(signal.SIGUSR1, passtochild)
|
||||||
|
signal.signal(signal.SIGUSR2, passtochild)
|
||||||
|
signal.signal(signal.SIGQUIT, passtochild)
|
||||||
|
signal.signal(signal.SIGTERM, passtochild)
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
patroni = subprocess.Popen([sys.executable] + sys.argv)
|
||||||
|
pid = patroni.pid
|
||||||
|
patroni.wait()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import urllib3
|
|||||||
from consul import ConsulException, NotFound, base
|
from consul import ConsulException, NotFound, base
|
||||||
from patroni.dcs import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member, SyncState
|
from patroni.dcs import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member, SyncState
|
||||||
from patroni.exceptions import DCSError
|
from patroni.exceptions import DCSError
|
||||||
from patroni.utils import Retry, RetryFailedError, sleep
|
from patroni.utils import Retry, RetryFailedError
|
||||||
from urllib3.exceptions import HTTPError
|
from urllib3.exceptions import HTTPError
|
||||||
from six.moves.urllib.parse import urlencode
|
from six.moves.urllib.parse import urlencode
|
||||||
from six.moves.http_client import HTTPException
|
from six.moves.http_client import HTTPException
|
||||||
@@ -121,7 +121,7 @@ class Consul(AbstractDCS):
|
|||||||
self.refresh_session()
|
self.refresh_session()
|
||||||
except ConsulError:
|
except ConsulError:
|
||||||
logger.info('waiting on consul')
|
logger.info('waiting on consul')
|
||||||
sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
def set_ttl(self, ttl):
|
def set_ttl(self, ttl):
|
||||||
if self._client.http.set_ttl(ttl/2.0): # Consul multiplies the TTL by 2x
|
if self._client.http.set_ttl(ttl/2.0): # Consul multiplies the TTL by 2x
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ from dns.exception import DNSException
|
|||||||
from dns import resolver
|
from dns import resolver
|
||||||
from patroni.dcs import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member, SyncState
|
from patroni.dcs import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member, SyncState
|
||||||
from patroni.exceptions import DCSError
|
from patroni.exceptions import DCSError
|
||||||
from patroni.utils import Retry, RetryFailedError, sleep
|
from patroni.utils import Retry, RetryFailedError
|
||||||
from urllib3.exceptions import HTTPError, ReadTimeoutError
|
from urllib3.exceptions import HTTPError, ReadTimeoutError
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
from six.moves.http_client import HTTPException
|
from six.moves.http_client import HTTPException
|
||||||
@@ -251,7 +251,7 @@ class Etcd(AbstractDCS):
|
|||||||
client = Client(config)
|
client = Client(config)
|
||||||
except etcd.EtcdException:
|
except etcd.EtcdException:
|
||||||
logger.info('waiting on etcd')
|
logger.info('waiting on etcd')
|
||||||
sleep(5)
|
time.sleep(5)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def set_ttl(self, ttl):
|
def set_ttl(self, ttl):
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import requests
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from patroni.dcs.zookeeper import ZooKeeper
|
from patroni.dcs.zookeeper import ZooKeeper
|
||||||
from patroni.utils import sleep
|
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -24,7 +23,7 @@ class ExhibitorEnsembleProvider(object):
|
|||||||
self._next_poll = None
|
self._next_poll = None
|
||||||
while not self.poll():
|
while not self.poll():
|
||||||
logger.info('waiting on exhibitor')
|
logger.info('waiting on exhibitor')
|
||||||
sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
if self._next_poll and self._next_poll > time.time():
|
if self._next_poll and self._next_poll > time.time():
|
||||||
|
|||||||
+8
-7
@@ -1,17 +1,18 @@
|
|||||||
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import requests
|
import requests
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
import time
|
||||||
from threading import RLock
|
|
||||||
|
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
from patroni.async_executor import AsyncExecutor
|
from patroni.async_executor import AsyncExecutor
|
||||||
from patroni.exceptions import DCSError, PostgresConnectionException
|
from patroni.exceptions import DCSError, PostgresConnectionException
|
||||||
from patroni.postgresql import ACTION_ON_START
|
from patroni.postgresql import ACTION_ON_START
|
||||||
from patroni.utils import polling_loop, sleep, tzutc
|
from patroni.utils import polling_loop, tzutc
|
||||||
|
from threading import RLock
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ class Ha(object):
|
|||||||
|
|
||||||
if picked and not allow_promote:
|
if picked and not allow_promote:
|
||||||
# Wait for PostgreSQL to enable synchronous mode and see if we can immediately set sync_standby
|
# Wait for PostgreSQL to enable synchronous mode and see if we can immediately set sync_standby
|
||||||
sleep(2)
|
time.sleep(2)
|
||||||
picked, allow_promote = self.state_handler.pick_synchronous_standby(self.cluster)
|
picked, allow_promote = self.state_handler.pick_synchronous_standby(self.cluster)
|
||||||
if allow_promote:
|
if allow_promote:
|
||||||
cluster = self.dcs.get_cluster()
|
cluster = self.dcs.get_cluster()
|
||||||
@@ -429,7 +430,7 @@ class Ha(object):
|
|||||||
self.state_handler.set_role('demoted')
|
self.state_handler.set_role('demoted')
|
||||||
self.dcs.delete_leader()
|
self.dcs.delete_leader()
|
||||||
self.dcs.reset_cluster()
|
self.dcs.reset_cluster()
|
||||||
sleep(2) # Give a time to somebody to take the leader lock
|
time.sleep(2) # Give a time to somebody to take the leader lock
|
||||||
cluster = self.dcs.get_cluster()
|
cluster = self.dcs.get_cluster()
|
||||||
node_to_follow = self._get_node_to_follow(cluster)
|
node_to_follow = self._get_node_to_follow(cluster)
|
||||||
return self.state_handler.follow(node_to_follow, cluster.leader, recovery=True, need_rewind=True)
|
return self.state_handler.follow(node_to_follow, cluster.leader, recovery=True, need_rewind=True)
|
||||||
@@ -459,7 +460,7 @@ class Ha(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# The value is very close to now
|
# The value is very close to now
|
||||||
sleep(max(delta, 0))
|
time.sleep(max(delta, 0))
|
||||||
logger.info('Manual scheduled {0} at %s'.format(action_name), scheduled_at.isoformat())
|
logger.info('Manual scheduled {0} at %s'.format(action_name), scheduled_at.isoformat())
|
||||||
return True
|
return True
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@@ -528,7 +529,7 @@ class Ha(object):
|
|||||||
# node tagged as nofailover can be ahead of the new leader either, but it is always excluded from elections
|
# node tagged as nofailover can be ahead of the new leader either, but it is always excluded from elections
|
||||||
need_rewind = bool(self.cluster.failover) or self.patroni.nofailover
|
need_rewind = bool(self.cluster.failover) or self.patroni.nofailover
|
||||||
if need_rewind:
|
if need_rewind:
|
||||||
sleep(2) # Give a time to somebody to take the leader lock
|
time.sleep(2) # Give a time to somebody to take the leader lock
|
||||||
|
|
||||||
if self.patroni.nofailover:
|
if self.patroni.nofailover:
|
||||||
return self.follow('demoting self because I am not allowed to become master',
|
return self.follow('demoting self because I am not allowed to become master',
|
||||||
|
|||||||
+4
-35
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -11,8 +10,6 @@ if sys.hexversion >= 0x3000000:
|
|||||||
long = int
|
long = int
|
||||||
|
|
||||||
tzutc = tz.tzutc()
|
tzutc = tz.tzutc()
|
||||||
__interrupted_sleep = False
|
|
||||||
__reap_children = False
|
|
||||||
|
|
||||||
|
|
||||||
def deep_compare(obj1, obj2):
|
def deep_compare(obj1, obj2):
|
||||||
@@ -197,36 +194,8 @@ def compare_values(vartype, unit, old_value, new_value):
|
|||||||
return old_value is not None and new_value is not None and old_value == new_value
|
return old_value is not None and new_value is not None and old_value == new_value
|
||||||
|
|
||||||
|
|
||||||
def sigchld_handler(signo, stack_frame):
|
def _sleep(interval):
|
||||||
global __interrupted_sleep, __reap_children
|
time.sleep(interval)
|
||||||
__reap_children = __interrupted_sleep = True
|
|
||||||
|
|
||||||
|
|
||||||
def sleep(interval):
|
|
||||||
global __interrupted_sleep
|
|
||||||
current_time = time.time()
|
|
||||||
end_time = current_time + interval
|
|
||||||
while current_time < end_time:
|
|
||||||
__interrupted_sleep = False
|
|
||||||
time.sleep(end_time - current_time)
|
|
||||||
if not __interrupted_sleep: # we will ignore only sigchld
|
|
||||||
break
|
|
||||||
current_time = time.time()
|
|
||||||
__interrupted_sleep = False
|
|
||||||
|
|
||||||
|
|
||||||
def reap_children():
|
|
||||||
global __reap_children
|
|
||||||
if __reap_children:
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
ret = os.waitpid(-1, os.WNOHANG)
|
|
||||||
if ret == (0, 0):
|
|
||||||
break
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
__reap_children = False
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pg_version(version):
|
def is_valid_pg_version(version):
|
||||||
@@ -243,7 +212,7 @@ class Retry(object):
|
|||||||
"""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,
|
||||||
sleep_func=sleep, deadline=None, retry_exceptions=PatroniException):
|
sleep_func=_sleep, deadline=None, retry_exceptions=PatroniException):
|
||||||
"""Create a :class:`Retry` instance for retrying function calls
|
"""Create a :class:`Retry` instance for retrying function calls
|
||||||
|
|
||||||
:param max_tries: How many times to retry the command. -1 means infinite tries.
|
:param max_tries: How many times to retry the command. -1 means infinite tries.
|
||||||
@@ -314,4 +283,4 @@ def polling_loop(timeout, interval=1):
|
|||||||
while time.time() < end_time:
|
while time.time() < end_time:
|
||||||
yield iteration
|
yield iteration
|
||||||
iteration += 1
|
iteration += 1
|
||||||
sleep(interval)
|
time.sleep(interval)
|
||||||
|
|||||||
+2
-2
@@ -514,7 +514,7 @@ class TestHa(unittest.TestCase):
|
|||||||
self.ha.load_cluster_from_dcs = Mock(side_effect=DCSError('Etcd is not responding properly'))
|
self.ha.load_cluster_from_dcs = Mock(side_effect=DCSError('Etcd is not responding properly'))
|
||||||
self.assertEquals(self.ha.run_cycle(), 'PAUSE: DCS is not accessible')
|
self.assertEquals(self.ha.run_cycle(), 'PAUSE: DCS is not accessible')
|
||||||
|
|
||||||
@patch('patroni.ha.sleep', Mock())
|
@patch('time.sleep', Mock())
|
||||||
def test_process_sync_replication(self):
|
def test_process_sync_replication(self):
|
||||||
self.ha.has_lock = true
|
self.ha.has_lock = true
|
||||||
mock_set_sync = self.p.set_synchronous_standby = Mock()
|
mock_set_sync = self.p.set_synchronous_standby = Mock()
|
||||||
@@ -634,7 +634,7 @@ class TestHa(unittest.TestCase):
|
|||||||
mock_promote.assert_called_once()
|
mock_promote.assert_called_once()
|
||||||
mock_write_sync.assert_called_once_with('other', None, index=0)
|
mock_write_sync.assert_called_once_with('other', None, index=0)
|
||||||
|
|
||||||
@patch('patroni.utils.sleep')
|
@patch('time.sleep')
|
||||||
def test_disable_sync_when_restarting(self, mock_sleep):
|
def test_disable_sync_when_restarting(self, mock_sleep):
|
||||||
self.ha.is_synchronous_mode = true
|
self.ha.is_synchronous_mode = true
|
||||||
|
|
||||||
|
|||||||
+36
-4
@@ -1,4 +1,5 @@
|
|||||||
import etcd
|
import etcd
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
@@ -8,7 +9,7 @@ from patroni.api import RestApiServer
|
|||||||
from patroni.async_executor import AsyncExecutor
|
from patroni.async_executor import AsyncExecutor
|
||||||
from patroni.dcs.etcd import Client
|
from patroni.dcs.etcd import Client
|
||||||
from patroni.exceptions import DCSError
|
from patroni.exceptions import DCSError
|
||||||
from patroni import Patroni, main as _main
|
from patroni import Patroni, main as _main, patroni_main
|
||||||
from six.moves import BaseHTTPServer
|
from six.moves import BaseHTTPServer
|
||||||
from test_etcd import SleepException, etcd_read, etcd_write
|
from test_etcd import SleepException, etcd_read, etcd_write
|
||||||
from test_postgresql import Postgresql, psycopg2_connect
|
from test_postgresql import Postgresql, psycopg2_connect
|
||||||
@@ -53,16 +54,47 @@ class TestPatroni(unittest.TestCase):
|
|||||||
@patch('time.sleep', Mock(side_effect=SleepException))
|
@patch('time.sleep', Mock(side_effect=SleepException))
|
||||||
@patch.object(etcd.Client, 'delete', Mock())
|
@patch.object(etcd.Client, 'delete', Mock())
|
||||||
@patch.object(Client, 'machines')
|
@patch.object(Client, 'machines')
|
||||||
def test_patroni_main(self, mock_machines):
|
def test_patroni_patroni_main(self, mock_machines):
|
||||||
with patch('subprocess.call', Mock(return_value=1)):
|
with patch('subprocess.call', Mock(return_value=1)):
|
||||||
sys.argv = ['patroni.py', 'postgres0.yml']
|
sys.argv = ['patroni.py', 'postgres0.yml']
|
||||||
|
|
||||||
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
|
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
|
||||||
with patch.object(Patroni, 'run', Mock(side_effect=SleepException)):
|
with patch.object(Patroni, 'run', Mock(side_effect=SleepException)):
|
||||||
self.assertRaises(SleepException, _main)
|
self.assertRaises(SleepException, patroni_main)
|
||||||
with patch.object(Patroni, 'run', Mock(side_effect=KeyboardInterrupt())):
|
with patch.object(Patroni, 'run', Mock(side_effect=KeyboardInterrupt())):
|
||||||
with patch('patroni.ha.Ha.is_paused', Mock(return_value=True)):
|
with patch('patroni.ha.Ha.is_paused', Mock(return_value=True)):
|
||||||
_main()
|
patroni_main()
|
||||||
|
|
||||||
|
@patch('os.getpid')
|
||||||
|
@patch('subprocess.Popen', )
|
||||||
|
@patch('patroni.patroni_main', Mock())
|
||||||
|
def test_patroni_main(self, mock_popen, mock_getpid):
|
||||||
|
mock_getpid.return_value = 2
|
||||||
|
_main()
|
||||||
|
|
||||||
|
mock_getpid.return_value = 1
|
||||||
|
|
||||||
|
def mock_signal(signo, handler):
|
||||||
|
handler(signo, None)
|
||||||
|
|
||||||
|
with patch('signal.signal', mock_signal):
|
||||||
|
with patch('os.waitpid', Mock(side_effect=[(1, 0), (0, 0)])):
|
||||||
|
_main()
|
||||||
|
with patch('os.waitpid', Mock(side_effect=OSError)):
|
||||||
|
_main()
|
||||||
|
|
||||||
|
ref = {'passtochild': lambda signo, stack_frame: 0}
|
||||||
|
|
||||||
|
def mock_sighup(signo, handler):
|
||||||
|
if signo == signal.SIGHUP:
|
||||||
|
ref['passtochild'] = handler
|
||||||
|
|
||||||
|
def mock_wait():
|
||||||
|
ref['passtochild'](0, None)
|
||||||
|
|
||||||
|
mock_popen.return_value.wait = mock_wait
|
||||||
|
with patch('signal.signal', mock_sighup), patch('os.kill', Mock()):
|
||||||
|
self.assertIsNone(_main())
|
||||||
|
|
||||||
@patch('patroni.config.Config.save_cache', Mock())
|
@patch('patroni.config.Config.save_cache', Mock())
|
||||||
@patch('patroni.config.Config.reload_local_configuration', Mock(return_value=True))
|
@patch('patroni.config.Config.reload_local_configuration', Mock(return_value=True))
|
||||||
|
|||||||
+1
-19
@@ -2,25 +2,7 @@ import unittest
|
|||||||
|
|
||||||
from mock import Mock, patch
|
from mock import Mock, patch
|
||||||
from patroni.exceptions import PatroniException
|
from patroni.exceptions import PatroniException
|
||||||
from patroni.utils import reap_children, Retry, RetryFailedError, sigchld_handler, sleep
|
from patroni.utils import Retry, RetryFailedError
|
||||||
|
|
||||||
|
|
||||||
def time_sleep(_):
|
|
||||||
sigchld_handler(None, None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
|
||||||
|
|
||||||
@patch('time.sleep', Mock())
|
|
||||||
def test_reap_children(self):
|
|
||||||
self.assertIsNone(reap_children())
|
|
||||||
with patch('os.waitpid', Mock(return_value=(0, 0))):
|
|
||||||
sigchld_handler(None, None)
|
|
||||||
self.assertIsNone(reap_children())
|
|
||||||
|
|
||||||
@patch('time.sleep', time_sleep)
|
|
||||||
def test_sleep(self):
|
|
||||||
self.assertIsNone(sleep(0.01))
|
|
||||||
|
|
||||||
|
|
||||||
@patch('time.sleep', Mock())
|
@patch('time.sleep', Mock())
|
||||||
|
|||||||
Reference in New Issue
Block a user