mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-09-01 17:19:31 +00:00
* Only activate watchdog while master and not paused We don't really need the protections while we are not master. This way we only need to tickle the watchdog when we are updating leader key or while demotion is happening. As implemented we might fail to notice to shut down the watchdog if someone demotes postgres and removes leader key behind Patroni's back. There are probably other similar cases. Basically if the administrator if being actively stupid they might get unexpected restarts. That seems fine. * Add configuration change support. Change MODE_REQUIRED to disable leader eligibility instead of closing Patroni. Changes watchdog timeout during the next keepalive when ttl is changed. Watchdog driver and requirement can also be switched online. When watchdog mode is `required` and watchdog setup does not work then the effect is similar to nofailover. Add watchdog_failed to status API to signify this. This is True only when watchdog does not work **AND** it is required. * Reset implementation when config changed while active. * Add watchdog safety margin configuration Defaults to 5 seconds. Basically this is the maximum amount of time that can pass between the calls to odcs.update_leader()` and `watchdog.keepalive()`, which are called right after each other. Should be safe for pretty much any sane scenario and allows the default settings to not trigger watchdog when DCS is not responding. * Cancel bootstrap if watchdog activation fails The system would have demoted itself anyway the next HA loop. Doing it in bootstrap gives at least some other node chance to try bootstrapping in the hope that it is configured correctly. If all nodes are unable to activate they will continue to try until the disk is filled with moved datadirs. Perhaps not ideal behavior, but as the situation is unlikely to resolve itself without administrator intervention it doesn't seem too bad.
204 lines
6.3 KiB
Python
204 lines
6.3 KiB
Python
import logging
|
|
import os
|
|
import signal
|
|
import sys
|
|
import time
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Patroni(object):
|
|
|
|
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__
|
|
from patroni.watchdog import Watchdog
|
|
|
|
self.setup_signal_handlers()
|
|
|
|
self.version = __version__
|
|
self.config = Config()
|
|
self.dcs = get_dcs(self.config)
|
|
self.watchdog = Watchdog(self.config)
|
|
self.load_dynamic_configuration()
|
|
|
|
self.postgresql = Postgresql(self.config['postgresql'])
|
|
self.api = RestApiServer(self, self.config['restapi'])
|
|
self.ha = Ha(self)
|
|
|
|
self.tags = self.get_tags()
|
|
self.next_run = time.time()
|
|
self.scheduled_restart = {}
|
|
|
|
def load_dynamic_configuration(self):
|
|
from patroni.exceptions import DCSError
|
|
while True:
|
|
try:
|
|
cluster = self.dcs.get_cluster()
|
|
if cluster and cluster.config:
|
|
if self.config.set_dynamic_configuration(cluster.config):
|
|
self.dcs.reload_config(self.config)
|
|
self.watchdog.reload_config(self.config)
|
|
elif not self.config.dynamic_configuration and 'bootstrap' in self.config:
|
|
if self.config.set_dynamic_configuration(self.config['bootstrap']['dcs']):
|
|
self.dcs.reload_config(self.config)
|
|
break
|
|
except DCSError:
|
|
logger.warning('Can not get cluster from dcs')
|
|
|
|
def get_tags(self):
|
|
return {tag: value for tag, value in self.config.get('tags', {}).items()
|
|
if tag not in ('clonefrom', 'nofailover', 'noloadbalance', 'nosync') or value}
|
|
|
|
@property
|
|
def nofailover(self):
|
|
return bool(self.tags.get('nofailover', False))
|
|
|
|
@property
|
|
def nosync(self):
|
|
return bool(self.tags.get('nosync', False))
|
|
|
|
def reload_config(self):
|
|
try:
|
|
self.tags = self.get_tags()
|
|
self.dcs.reload_config(self.config)
|
|
self.watchdog.reload_config(self.config)
|
|
self.api.reload_config(self.config['restapi'])
|
|
self.postgresql.reload_config(self.config['postgresql'])
|
|
except Exception:
|
|
logger.exception('Failed to reload config_file=%s', self.config.config_file)
|
|
|
|
@property
|
|
def replicatefrom(self):
|
|
return self.tags.get('replicatefrom')
|
|
|
|
def sighup_handler(self, *args):
|
|
self._received_sighup = True
|
|
|
|
def sigterm_handler(self, *args):
|
|
if not self._received_sigterm:
|
|
self._received_sigterm = True
|
|
sys.exit()
|
|
|
|
@property
|
|
def noloadbalance(self):
|
|
return bool(self.tags.get('noloadbalance', False))
|
|
|
|
def schedule_next_run(self):
|
|
self.next_run += self.dcs.loop_wait
|
|
current_time = time.time()
|
|
nap_time = self.next_run - current_time
|
|
if nap_time <= 0:
|
|
self.next_run = current_time
|
|
# Release the GIL so we don't starve anyone waiting on async_executor lock
|
|
time.sleep(0.001)
|
|
# Warn user that Patroni is not keeping up
|
|
logger.warning("Loop time exceeded, rescheduling immediately.")
|
|
elif self.ha.watch(nap_time):
|
|
self.next_run = time.time()
|
|
|
|
def run(self):
|
|
self.api.start()
|
|
self.next_run = time.time()
|
|
|
|
while not self._received_sigterm:
|
|
if self._received_sighup:
|
|
self._received_sighup = False
|
|
if self.config.reload_local_configuration():
|
|
self.reload_config()
|
|
|
|
logger.info(self.ha.run_cycle())
|
|
|
|
cluster = self.dcs.cluster
|
|
if cluster and cluster.config and self.config.set_dynamic_configuration(cluster.config):
|
|
self.reload_config()
|
|
|
|
if self.postgresql.role != 'uninitialized':
|
|
self.config.save_cache()
|
|
|
|
self.schedule_next_run()
|
|
|
|
def setup_signal_handlers(self):
|
|
self._received_sighup = False
|
|
self._received_sigterm = False
|
|
signal.signal(signal.SIGHUP, self.sighup_handler)
|
|
signal.signal(signal.SIGTERM, self.sigterm_handler)
|
|
|
|
def shutdown(self):
|
|
self.api.shutdown()
|
|
self.ha.shutdown()
|
|
|
|
|
|
def patroni_main():
|
|
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
|
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
|
|
|
patroni = Patroni()
|
|
try:
|
|
patroni.run()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
patroni.shutdown()
|
|
|
|
|
|
def pg_ctl_start(args):
|
|
import subprocess
|
|
postmaster = subprocess.Popen(args)
|
|
print(postmaster.pid)
|
|
|
|
|
|
def call_self(args, **kwargs):
|
|
"""This function executes Patroni once again with provided arguments.
|
|
|
|
:args: list of arguments to call Patroni with.
|
|
:returns: `Popen` object"""
|
|
|
|
exe = [sys.executable]
|
|
if not getattr(sys, 'frozen', False): # Binary distribution?
|
|
exe.append(sys.argv[0])
|
|
|
|
import subprocess
|
|
return subprocess.Popen(exe + args, **kwargs)
|
|
|
|
|
|
def main():
|
|
if os.getpid() != 1:
|
|
if len(sys.argv) > 5 and sys.argv[1] == 'pg_ctl_start':
|
|
return pg_ctl_start(sys.argv[2:])
|
|
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)
|
|
|
|
patroni = call_self(sys.argv[1:])
|
|
pid = patroni.pid
|
|
patroni.wait()
|