From e106bf82a8a403e5e882b2b0438539fc2b2e8070 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 15 Jan 2014 19:08:34 -0800 Subject: [PATCH] Add --reload option for code reloading Fix #526 --- NOTICE | 28 +++++++++++++++ examples/example_gevent_reloader.py | 35 ------------------- gunicorn/config.py | 15 ++++++++ gunicorn/reloader.py | 54 +++++++++++++++++++++++++++++ gunicorn/workers/base.py | 9 +++++ 5 files changed, 106 insertions(+), 35 deletions(-) delete mode 100644 examples/example_gevent_reloader.py create mode 100644 gunicorn/reloader.py diff --git a/NOTICE b/NOTICE index fa8188f7..f990507b 100644 --- a/NOTICE +++ b/NOTICE @@ -54,6 +54,34 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +gunicorn.reloader +----------------- + +Based on greins.reloader module under MIT license: + +2010 (c) Meebo, Inc. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + doc/sitemap_gen.py ------------------ Under BSD License : diff --git a/examples/example_gevent_reloader.py b/examples/example_gevent_reloader.py deleted file mode 100644 index b303c2f0..00000000 --- a/examples/example_gevent_reloader.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import os -import signal -import sys - -def on_starting(server): - # use server hook to patch socket to allow worker reloading - from gevent import monkey - monkey.patch_socket() - -def when_ready(server): - def monitor(): - modify_times = {} - while True: - for module in sys.modules.values(): - path = getattr(module, "__file__", None) - if not path: continue - if path.endswith(".pyc") or path.endswith(".pyo"): - path = path[:-1] - try: - modified = os.stat(path).st_mtime - except: - continue - if path not in modify_times: - modify_times[path] = modified - continue - if modify_times[path] != modified: - logging.info("%s modified; restarting server", path) - os.kill(os.getpid(), signal.SIGHUP) - modify_times = {} - break - gevent.sleep(1) - - import gevent - gevent.spawn(monitor) diff --git a/gunicorn/config.py b/gunicorn/config.py index 51115f2b..6dbb25ed 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -706,6 +706,21 @@ class Debug(Setting): """ +class Reload(Setting): + name = "reload" + section = 'Debugging' + cli = ['--reload'] + validator = validate_bool + action = 'store_true' + default = False + desc = '''\ + Restart workers when code changes. + + This setting is intended for development. It will cause workers to be + restarted whenever application code changes. + ''' + + class Spew(Setting): name = "spew" section = "Debugging" diff --git a/gunicorn/reloader.py b/gunicorn/reloader.py new file mode 100644 index 00000000..4333ef7f --- /dev/null +++ b/gunicorn/reloader.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 - +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import re +import signal +import sys +import time +import threading + + +class Reloader(threading.Thread): + def __init__(self, extra_files=None, interval=1, callback=None): + super(Reloader, self).__init__() + self.setDaemon(True) + self._extra_files = set(extra_files or ()) + self._extra_files_lock = threading.RLock() + self._interval = interval + self._callback = callback + + def add_extra_file(self, filename): + with self._extra_files_lock: + self._extra_files.add(filename) + + def get_files(self): + fnames = [ + re.sub('py[co]$', 'py', module.__file__) + for module in sys.modules.values() + if hasattr(module, '__file__') + ] + + with self._extra_files_lock: + fnames.extend(self._extra_files) + + return fnames + + def run(self): + mtimes = {} + while True: + for filename in self.get_files(): + try: + mtime = os.stat(filename).st_mtime + except OSError: + continue + old_time = mtimes.get(filename) + if old_time is None: + mtimes[filename] = mtime + continue + elif mtime > old_time: + if self._callback: + self._callback(filename) + time.sleep(self._interval) diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py index 3e775c07..27ef0db5 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -12,6 +12,7 @@ import traceback from gunicorn import util from gunicorn.workers.workertmp import WorkerTmp +from gunicorn.reloader import Reloader from gunicorn.http.errors import InvalidHeader, InvalidHeaderName, \ InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, \ LimitRequestLine, LimitRequestHeaders @@ -79,6 +80,14 @@ class Worker(object): loop is initiated. """ + # start the reloader + if self.cfg.reload: + def changed(fname): + self.log.info("Worker reloading: %s modified", fname) + os.kill(self.pid, signal.SIGTERM) + raise SystemExit() + Reloader(callback=changed).start() + # set enviroment' variables if self.cfg.env: for k, v in self.cfg.env.items():