From 64b26ef76648ad2a70f64379f0f5433117450529 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 4 Oct 2016 12:08:14 -0500 Subject: [PATCH 1/3] Fix #1368 by adding InotifyReloader and 'use-inotify' configuration option Currently, '--reload' uses FS polling to find out when files have changed. For some time, the Linux kernel has had a feature called inotify that allows applications to monitor for FS events without polling. This commit adds a new 'use-inotify' configuration option that will cause gunicorn to use the new 'InotifyReloader' instead of the default 'Reloader' when 'reload' is enabled. Using inotify can result in lower CPU consumption by gunicorn especially when working with virtualized filesystems or environments with a large number of watched files / directories. --- THANKS | 1 + gunicorn/config.py | 16 +++++++++++ gunicorn/reloader.py | 62 ++++++++++++++++++++++++++++++++++++++++ gunicorn/workers/base.py | 27 ++++++++--------- 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/THANKS b/THANKS index 49cff951..c3b99cec 100644 --- a/THANKS +++ b/THANKS @@ -162,3 +162,4 @@ Wolfgang Schnerring Jason Madden Eugene Obukhov Jan-Philip Gehrcke +Mark Adams diff --git a/gunicorn/config.py b/gunicorn/config.py index ce8b41d8..02489a90 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -820,6 +820,22 @@ class Reload(Setting): application code or the reload will not work as designed. ''' +class ReloadInotify(Setting): + name = 'inotify' + section = 'Debugging' + cli = ['--use-inotify'] + validator = validate_bool + action = "store_true" + default = False + + desc = '''\ + When using the 'reload' option, use the kernel's inotify APIs to watch + files instead of polling the filesystem. On many systems this could result + in a performance improvement when using 'reload'. + + This setting must be used in conjunction with 'reload' and requires the + 'inotify' package be installed from PyPI. + ''' class Spew(Setting): name = "spew" diff --git a/gunicorn/reloader.py b/gunicorn/reloader.py index 130e3d60..b5e7679f 100644 --- a/gunicorn/reloader.py +++ b/gunicorn/reloader.py @@ -4,6 +4,7 @@ # See the NOTICE for more information. import os +import os.path import re import sys import time @@ -51,3 +52,64 @@ class Reloader(threading.Thread): if self._callback: self._callback(filename) time.sleep(self._interval) + +try: + from inotify.adapters import Inotify + import inotify.constants + has_inotify = True +except ImportError: + has_inotify = False + +class InotifyReloader(): + def __init__(self, callback=None): + raise ImportError('You must have the inotify module installed to use the INotify reloader') + +if has_inotify: + + class InotifyReloader(threading.Thread): + event_mask = (inotify.constants.IN_CREATE | inotify.constants.IN_DELETE + | inotify.constants.IN_DELETE_SELF | inotify.constants.IN_MODIFY + | inotify.constants.IN_MOVE_SELF | inotify.constants.IN_MOVED_FROM + | inotify.constants.IN_MOVED_TO) + + def __init__(self, extra_files=None, callback=None): + super(InotifyReloader, self).__init__() + self.setDaemon(True) + self._callback = callback + self._dirs = set() + self._watcher = Inotify() + + def add_extra_file(self, filename): + dirname = os.path.dirname(filename) + + if dirname in self._dirs: + return + + self._watcher.add_watch(dirname, mask=self.event_mask) + self._dirs.add(dirname) + + def get_dirs(self): + fnames = [ + os.path.dirname(re.sub('py[co]$', 'py', module.__file__)) + for module in list(sys.modules.values()) + if hasattr(module, '__file__') + ] + + return set(fnames) + + def run(self): + self._dirs = self.get_dirs() + + for dirname in self._dirs: + self._watcher.add_watch(dirname, mask=self.event_mask) + + for event in self._watcher.event_gen(): + if event is None: + continue + + types = event[1] + filename = event[3] + + self._callback(filename) + + diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py index 4ceb691b..887ad8d6 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -14,7 +14,7 @@ import traceback from gunicorn import util from gunicorn.workers.workertmp import WorkerTmp -from gunicorn.reloader import Reloader +from gunicorn.reloader import Reloader, InotifyReloader, has_inotify from gunicorn.http.errors import ( InvalidHeader, InvalidHeaderName, InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders, @@ -85,18 +85,6 @@ class Worker(object): loop is initiated. """ - # start the reloader - if self.cfg.reload: - def changed(fname): - self.log.info("Worker reloading: %s modified", fname) - self.alive = False - self.cfg.worker_int(self) - time.sleep(0.1) - sys.exit(0) - - self.reloader = Reloader(callback=changed) - self.reloader.start() - # set environment' variables if self.cfg.env: for k, v in self.cfg.env.items(): @@ -126,6 +114,19 @@ class Worker(object): self.load_wsgi() + # start the reloader + if self.cfg.reload: + def changed(fname): + self.log.info("Worker reloading: %s modified", fname) + self.alive = False + self.cfg.worker_int(self) + time.sleep(0.1) + sys.exit(0) + + reloader_cls = Reloader if not self.cfg.inotify else InotifyReloader + self.reloader = reloader_cls(callback=changed) + self.reloader.start() + self.cfg.post_worker_init(self) # Enter main run loop From 92d48256e4c24edf75a7de13226e16bf88c21664 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Mon, 24 Oct 2016 09:08:02 -0500 Subject: [PATCH 2/3] Modify 'reload' config to be more consistent with existing API --reload = Runs the reloader with inotify if available and falls back on FS polling. --reload=inotify = Forces the reloader to run with inotify --reload=poll = Forces the reloader to use FS polling --- gunicorn/config.py | 43 ++++++++++++++++++++++------------------ gunicorn/reloader.py | 18 +++++++++-------- gunicorn/workers/base.py | 12 ++++++++--- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/gunicorn/config.py b/gunicorn/config.py index 02489a90..ff6c40ec 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -489,6 +489,19 @@ def validate_hostport(val): else: raise TypeError("Value must consist of: hostname:port") + +def validate_reloader(val): + if val is None: + val = 'default' + + choices = ['poll', 'inotify', 'default'] + + if val not in choices: + raise ConfigError( + 'Invalid reloader type. Must be one of: %s' % choices + ) + + def get_default_config_file(): config_path = os.path.join(os.path.abspath(os.getcwd()), 'gunicorn.conf.py') @@ -806,9 +819,11 @@ class Reload(Setting): name = "reload" section = 'Debugging' cli = ['--reload'] - validator = validate_bool - action = 'store_true' - default = False + validator = validate_reloader + nargs = '?' + const = 'default' + default = None + desc = '''\ Restart workers when code changes. @@ -818,24 +833,14 @@ class Reload(Setting): The reloader is incompatible with application preloading. When using a paste configuration be sure that the server block does not import any application code or the reload will not work as designed. + + When using this option, you can optionally specify whether you would + like to use file system polling or the kernel's inotify API to watch + for changes. Generally, inotify should be preferred if available + because it consumes less system resources. If no preference is given, + inotify will attempted with a fallback to FS polling.' ''' -class ReloadInotify(Setting): - name = 'inotify' - section = 'Debugging' - cli = ['--use-inotify'] - validator = validate_bool - action = "store_true" - default = False - - desc = '''\ - When using the 'reload' option, use the kernel's inotify APIs to watch - files instead of polling the filesystem. On many systems this could result - in a performance improvement when using 'reload'. - - This setting must be used in conjunction with 'reload' and requires the - 'inotify' package be installed from PyPI. - ''' class Spew(Setting): name = "spew" diff --git a/gunicorn/reloader.py b/gunicorn/reloader.py index b5e7679f..86f8f88f 100644 --- a/gunicorn/reloader.py +++ b/gunicorn/reloader.py @@ -60,18 +60,20 @@ try: except ImportError: has_inotify = False + class InotifyReloader(): def __init__(self, callback=None): - raise ImportError('You must have the inotify module installed to use the INotify reloader') + raise ImportError('You must have the inotify module installed to use ' + 'the inotify reloader') if has_inotify: class InotifyReloader(threading.Thread): - event_mask = (inotify.constants.IN_CREATE | inotify.constants.IN_DELETE - | inotify.constants.IN_DELETE_SELF | inotify.constants.IN_MODIFY - | inotify.constants.IN_MOVE_SELF | inotify.constants.IN_MOVED_FROM + event_mask = (inotify.constants.IN_CREATE | inotify.constants.IN_DELETE + | inotify.constants.IN_DELETE_SELF | inotify.constants.IN_MODIFY + | inotify.constants.IN_MOVE_SELF | inotify.constants.IN_MOVED_FROM | inotify.constants.IN_MOVED_TO) - + def __init__(self, extra_files=None, callback=None): super(InotifyReloader, self).__init__() self.setDaemon(True) @@ -102,14 +104,14 @@ if has_inotify: for dirname in self._dirs: self._watcher.add_watch(dirname, mask=self.event_mask) - + for event in self._watcher.event_gen(): if event is None: continue - - types = event[1] + filename = event[3] self._callback(filename) +preferred_reloader = InotifyReloader if has_inotify else Reloader diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py index 887ad8d6..885ff6d4 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -14,7 +14,7 @@ import traceback from gunicorn import util from gunicorn.workers.workertmp import WorkerTmp -from gunicorn.reloader import Reloader, InotifyReloader, has_inotify +from gunicorn.reloader import preferred_reloader, Reloader, InotifyReloader from gunicorn.http.errors import ( InvalidHeader, InvalidHeaderName, InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders, @@ -122,8 +122,14 @@ class Worker(object): self.cfg.worker_int(self) time.sleep(0.1) sys.exit(0) - - reloader_cls = Reloader if not self.cfg.inotify else InotifyReloader + + if self.cfg.reload == 'poll': + reloader_cls = Reloader + elif self.cfg.reload == 'inotify': + reloader_cls = InotifyReloader + else: + reloader_cls = preferred_reloader + self.reloader = reloader_cls(callback=changed) self.reloader.start() From b98205f896ff4fd0d8f6bb78cdb9cfb778aba9f7 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 25 Oct 2016 08:29:05 -0500 Subject: [PATCH 3/3] Update documentation to indicate inotify requirement for reloader --- docs/source/settings.rst | 13 +++++++++++-- gunicorn/config.py | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/source/settings.rst b/docs/source/settings.rst index b39c0c4a..90bf9b74 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -255,8 +255,8 @@ Debugging reload ~~~~~~ -* ``--reload`` -* ``False`` +* ``--reload RELOADER_TYPE`` +* ``None`` Restart workers when code changes. @@ -267,6 +267,15 @@ The reloader is incompatible with application preloading. When using a paste configuration be sure that the server block does not import any application code or the reload will not work as designed. +When using this option, you can optionally specify whether you would +like to use file system polling or the kernel's inotify API to watch +for changes. Generally, inotify should be preferred if available +because it consumes less system resources. If no preference is given, +inotify will attempted with a fallback to FS polling. + +Note: In order to use the inotify reloader, you must have the 'inotify' +package installed. + spew ~~~~ diff --git a/gunicorn/config.py b/gunicorn/config.py index ff6c40ec..60e93bf7 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -823,6 +823,7 @@ class Reload(Setting): nargs = '?' const = 'default' default = None + meta = 'RELOADER_TYPE' desc = '''\ Restart workers when code changes. @@ -838,7 +839,10 @@ class Reload(Setting): like to use file system polling or the kernel's inotify API to watch for changes. Generally, inotify should be preferred if available because it consumes less system resources. If no preference is given, - inotify will attempted with a fallback to FS polling.' + inotify will attempted with a fallback to FS polling. + + Note: In order to use the inotify reloader, you must have the 'inotify' + package installed. '''