new HUP signal handling. Rather than reexecuting the app it mimic NGINX

behavior :

Configuration reload
Start the new worker processes with a new configuration
Gracefully shutdown the old worker processes

To reload gunicorn code use USR2 signal.
This commit is contained in:
benoitc 2010-06-16 11:10:06 +02:00
parent 645938f2b9
commit 05b6281e8d
2 changed files with 125 additions and 80 deletions

View File

@ -18,14 +18,27 @@ class Application(object):
An application interface for configuring and loading An application interface for configuring and loading
the various necessities for any given web framework. the various necessities for any given web framework.
""" """
LOG_LEVELS = {
"critical": logging.CRITICAL,
"error": logging.ERROR,
"warning": logging.WARNING,
"info": logging.INFO,
"debug": logging.DEBUG
}
def __init__(self, usage=None): def __init__(self, usage=None):
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
self.cfg = Config(usage) self.cfg = Config(usage)
self.callable = None self.callable = None
self.logger = None
parser = self.cfg.parser() self.cfgparser = self.cfg.parser()
opts, args = parser.parse_args() self.opts, self.args = self.cfgparser.parse_args()
cfg = self.init(parser, opts, args)
self.load_config()
def load_config(self):
cfg = self.init(self.cfgparser, self.opts, self.args)
# Load up the any app specific configuration # Load up the any app specific configuration
if cfg: if cfg:
@ -33,18 +46,18 @@ class Application(object):
self.cfg.set(k.lower(), v) self.cfg.set(k.lower(), v)
# Load up the config file if its found. # Load up the config file if its found.
if opts.config and os.path.exists(opts.config): if self.opts.config and os.path.exists(self.opts.config):
cfg = { cfg = {
"__builtins__": __builtins__, "__builtins__": __builtins__,
"__name__": "__config__", "__name__": "__config__",
"__file__": opts.config, "__file__": self.opts.config,
"__doc__": None, "__doc__": None,
"__package__": None "__package__": None
} }
try: try:
execfile(opts.config, cfg, cfg) execfile(self.opts.config, cfg, cfg)
except Exception, e: except Exception, e:
print "Failed to read config file: %s" % opts.config print "Failed to read config file: %s" % self.opts.config
traceback.print_exc() traceback.print_exc()
sys.exit(1) sys.exit(1)
@ -60,7 +73,7 @@ class Application(object):
# Lastly, update the configuration with any command line # Lastly, update the configuration with any command line
# settings. # settings.
for k, v in list(opts.__dict__.items()): for k, v in list(self.opts.__dict__.items()):
if v is None: if v is None:
continue continue
self.cfg.set(k.lower(), v) self.cfg.set(k.lower(), v)
@ -71,6 +84,13 @@ class Application(object):
def load(self): def load(self):
raise NotImplementedError raise NotImplementedError
def reload(self):
self.load_config()
if self.cfg.spew:
debug.spew()
loglevel = self.LOG_LEVELS.get(self.cfg.loglevel.lower(), logging.INFO)
self.logger.setLevel(loglevel)
def wsgi(self): def wsgi(self):
if self.callable is None: if self.callable is None:
self.callable = self.load() self.callable = self.load()
@ -90,7 +110,7 @@ class Application(object):
"""\ """\
Set the log level and choose the destination for log output. Set the log level and choose the destination for log output.
""" """
logger = logging.getLogger('gunicorn') self.logger = logging.getLogger('gunicorn')
handlers = [] handlers = []
if self.cfg.logfile != "-": if self.cfg.logfile != "-":
@ -98,21 +118,13 @@ class Application(object):
else: else:
handlers.append(logging.StreamHandler()) handlers.append(logging.StreamHandler())
levels = { loglevel = self.LOG_LEVELS.get(self.cfg.loglevel.lower(), logging.INFO)
"critical": logging.CRITICAL, self.logger.setLevel(loglevel)
"error": logging.ERROR,
"warning": logging.WARNING,
"info": logging.INFO,
"debug": logging.DEBUG
}
loglevel = levels.get(self.cfg.loglevel.lower(), logging.INFO)
logger.setLevel(loglevel)
format = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s" format = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s"
datefmt = r"%Y-%m-%d %H:%M:%S" datefmt = r"%Y-%m-%d %H:%M:%S"
for h in handlers: for h in handlers:
h.setFormatter(logging.Formatter(format, datefmt)) h.setFormatter(logging.Formatter(format, datefmt))
logger.addHandler(h) self.logger.addHandler(h)

View File

@ -18,6 +18,10 @@ from gunicorn.pidfile import Pidfile
from gunicorn.sock import create_socket from gunicorn.sock import create_socket
from gunicorn import util from gunicorn import util
class HUPSignal(Exception):
pass
class Arbiter(object): class Arbiter(object):
""" """
Arbiter maintain the workers processes alive. It launches or Arbiter maintain the workers processes alive. It launches or
@ -48,21 +52,12 @@ class Arbiter(object):
) )
def __init__(self, app): def __init__(self, app):
self.app = app self.setup(app)
self.cfg = app.cfg
if self.cfg.preload_app:
self.app.wsgi()
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
self.address = self.cfg.address
self.num_workers = self.cfg.workers
self.debug = self.cfg.debug
self.timeout = self.cfg.timeout
self.proc_name = self.cfg.proc_name
self.worker_class = self.cfg.worker_class
self.pidfile = None self.pidfile = None
self.worker_age = 0 self.worker_age = 0
self.reexec_pid = 0 self.reexec_pid = 0
@ -89,13 +84,27 @@ class Arbiter(object):
0: sys.executable 0: sys.executable
} }
def setup(self, app):
self.app = app
self.cfg = app.cfg
self.address = self.cfg.address
self.num_workers = self.cfg.workers
self.debug = self.cfg.debug
self.timeout = self.cfg.timeout
self.proc_name = self.cfg.proc_name
self.worker_class = self.cfg.worker_class
if self.cfg.preload_app:
self.app.wsgi()
def start(self): def start(self):
"""\ """\
Initialize the arbiter. Start listening and set pidfile if needed. Initialize the arbiter. Start listening and set pidfile if needed.
""" """
self.pid = os.getpid() self.pid = os.getpid()
self.init_signals() self.init_signals()
self.LISTENER = create_socket(self.cfg) if not self.LISTENER:
self.LISTENER = create_socket(self.cfg)
if self.cfg.pidfile is not None: if self.cfg.pidfile is not None:
self.pidfile = Pidfile(self.cfg.pidfile) self.pidfile = Pidfile(self.cfg.pidfile)
@ -110,7 +119,7 @@ class Arbiter(object):
are queued. Child signals only wake up the master. are queued. Child signals only wake up the master.
""" """
if self.PIPE: if self.PIPE:
map(lambda p: p.close(), self.PIPE) map(lambda p: os.close(p), self.PIPE)
self.PIPE = pair = os.pipe() self.PIPE = pair = os.pipe()
map(util.set_non_blocking, pair) map(util.set_non_blocking, pair)
map(util.close_on_exec, pair) map(util.close_on_exec, pair)
@ -151,10 +160,12 @@ class Arbiter(object):
self.log.info("Handling signal: %s" % signame) self.log.info("Handling signal: %s" % signame)
handler() handler()
self.wakeup() self.wakeup()
except HUPSignal:
return self.reload()
except StopIteration: except StopIteration:
break self.halt()
except KeyboardInterrupt: except KeyboardInterrupt:
break self.halt()
except Exception: except Exception:
self.log.info("Unhandled exception in main loop:\n%s" % self.log.info("Unhandled exception in main loop:\n%s" %
traceback.format_exc()) traceback.format_exc())
@ -163,12 +174,6 @@ class Arbiter(object):
self.pidfile.unlink() self.pidfile.unlink()
sys.exit(-1) sys.exit(-1)
self.stop()
self.log.info("Shutting down: %s" % self.master_name)
if self.pidfile is not None:
self.pidfile.unlink()
sys.exit(0)
def handle_chld(self, sig, frame): def handle_chld(self, sig, frame):
"SIGCHLD handling" "SIGCHLD handling"
self.wakeup() self.wakeup()
@ -181,8 +186,7 @@ class Arbiter(object):
restart the workers and rereading the configuration. restart the workers and rereading the configuration.
""" """
self.log.info("Hang up: %s" % self.master_name) self.log.info("Hang up: %s" % self.master_name)
self.reexec() raise HUPSignal
raise StopIteration
def handle_quit(self): def handle_quit(self):
"SIGQUIT handling" "SIGQUIT handling"
@ -250,6 +254,14 @@ class Arbiter(object):
if e.errno not in [errno.EAGAIN, errno.EINTR]: if e.errno not in [errno.EAGAIN, errno.EINTR]:
raise raise
def halt(self):
""" halt arbiter """
self.stop()
self.log.info("Shutting down: %s" % self.master_name)
if self.pidfile is not None:
self.pidfile.unlink()
sys.exit(0)
def sleep(self): def sleep(self):
"""\ """\
Sleep until PIPE is readable or we timeout. Sleep until PIPE is readable or we timeout.
@ -270,6 +282,7 @@ class Arbiter(object):
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit() sys.exit()
def stop(self, graceful=True): def stop(self, graceful=True):
"""\ """\
Stop workers Stop workers
@ -305,6 +318,23 @@ class Arbiter(object):
self.cfg.pre_exec(self) self.cfg.pre_exec(self)
os.execvpe(self.START_CTX[0], self.START_CTX['args'], os.environ) os.execvpe(self.START_CTX[0], self.START_CTX['args'], os.environ)
def reload(self):
# reload conf
self.app.reload()
# spawn new workers
for i in range(self.app.cfg.workers):
self.spawn_worker()
# unlink pidfile
if self.pidfile is not None:
self.pidfile.unlink()
self.setup(self.app)
self.manage_workers()
return self.run()
def murder_workers(self): def murder_workers(self):
"""\ """\
Kill unused/idle workers Kill unused/idle workers
@ -361,6 +391,40 @@ class Arbiter(object):
pid, age = wpid, worker.age pid, age = wpid, worker.age
self.kill_worker(pid, signal.SIGQUIT) self.kill_worker(pid, signal.SIGQUIT)
def spawn_worker(self):
self.worker_age += 1
worker = self.worker_class(self.worker_age, self.pid, self.LISTENER,
self.app, self.timeout/2.0, self.cfg)
self.cfg.pre_fork(self, worker)
pid = os.fork()
if pid != 0:
self.WORKERS[pid] = worker
return
# Process Child
worker_pid = os.getpid()
try:
util._setproctitle("worker [%s]" % self.proc_name)
self.log.debug("Booting worker: %s (age: %s)" % (
worker_pid, self.worker_age))
self.cfg.post_fork(self, worker)
worker.init_process()
sys.exit(0)
except SystemExit:
raise
except:
self.log.exception("Exception in worker process:")
if not worker.booted:
sys.exit(self.WORKER_BOOT_ERROR)
sys.exit(-1)
finally:
self.log.info("Worker exiting (pid: %s)" % worker_pid)
try:
worker.tmp.close()
os.unlink(worker.tmpname)
except:
pass
def spawn_workers(self): def spawn_workers(self):
"""\ """\
Spawn new workers as needed. Spawn new workers as needed.
@ -370,38 +434,7 @@ class Arbiter(object):
""" """
for i in range(self.num_workers - len(self.WORKERS.keys())): for i in range(self.num_workers - len(self.WORKERS.keys())):
self.worker_age += 1 self.spawn_worker()
worker = self.worker_class(self.worker_age, self.pid, self.LISTENER,
self.app, self.timeout/2.0, self.cfg)
self.cfg.pre_fork(self, worker)
pid = os.fork()
if pid != 0:
self.WORKERS[pid] = worker
continue
# Process Child
worker_pid = os.getpid()
try:
util._setproctitle("worker [%s]" % self.proc_name)
self.log.debug("Booting worker: %s (age: %s)" % (
worker_pid, self.worker_age))
self.cfg.post_fork(self, worker)
worker.init_process()
sys.exit(0)
except SystemExit:
raise
except:
self.log.exception("Exception in worker process:")
if not worker.booted:
sys.exit(self.WORKER_BOOT_ERROR)
sys.exit(-1)
finally:
self.log.info("Worker exiting (pid: %s)" % worker_pid)
try:
worker.tmp.close()
os.unlink(worker.tmpname)
except:
pass
def kill_workers(self, sig): def kill_workers(self, sig):
"""\ """\