Repository: benoitc/gunicorn

Issue:
Add --reload-engine option
See https://github.com/benoitc/gunicorn/issues/1422#issuecomment-275669022 for context.


Discussion:

Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -23,6 +23,7 @@
 from gunicorn import __version__
 from gunicorn import _compat
 from gunicorn.errors import ConfigError
+from gunicorn.reloader import reloader_engines
 from gunicorn import six
 from gunicorn import util
 
@@ -496,6 +497,13 @@ def validate_hostport(val):
         raise TypeError("Value must consist of: hostname:port")
 
 
+def validate_reload_engine(val):
+    if val not in reloader_engines:
+        raise ConfigError("Invalid reload_engine: %r" % val)
+
+    return val
+
+
 def get_default_config_file():
     config_path = os.path.join(os.path.abspath(os.getcwd()),
             'gunicorn.conf.py')
@@ -838,6 +846,26 @@ class Reload(Setting):
         '''
 
 
+class ReloadEngine(Setting):
+    name = "reload_engine"
+    section = "Debugging"
+    cli = ["--reload-engine"]
+    meta = "STRING"
+    validator = validate_reload_engine
+    default = "auto"
+    desc = """\
+        The implementation that should be used to power :ref:`reload`.
+
+        Valid engines are:
+
+        * 'auto'
+        * 'poll'
+        * 'inotify' (requires inotify)
+
+        .. versionadded:: 19.7
+        """
+
+
 class Spew(Setting):
     name = "spew"
     section = "Debugging"
diff --git a/gunicorn/reloader.py b/gunicorn/reloader.py
--- a/gunicorn/reloader.py
+++ b/gunicorn/reloader.py
@@ -115,3 +115,9 @@ def run(self):
 
 
 preferred_reloader = InotifyReloader if has_inotify else Reloader
+
+reloader_engines = {
+    'auto': preferred_reloader,
+    'poll': Reloader,
+    'inotify': InotifyReloader,
+}
diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py
--- a/gunicorn/workers/base.py
+++ b/gunicorn/workers/base.py
@@ -15,7 +15,7 @@
 from gunicorn import six
 from gunicorn import util
 from gunicorn.workers.workertmp import WorkerTmp
-from gunicorn.reloader import preferred_reloader
+from gunicorn.reloader import reloader_engines
 from gunicorn.http.errors import (
     InvalidHeader, InvalidHeaderName, InvalidRequestLine, InvalidRequestMethod,
     InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders,
@@ -119,7 +119,8 @@ def changed(fname):
                 time.sleep(0.1)
                 sys.exit(0)
 
-            self.reloader = preferred_reloader(callback=changed)
+            reloader_cls = reloader_engines[self.cfg.reload_engine]
+            self.reloader = reloader_cls(callback=changed)
             self.reloader.start()
 
         self.load_wsgi()


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -10,6 +10,7 @@
 
 from gunicorn import config
 from gunicorn.app.base import Application
+from gunicorn.errors import ConfigError
 from gunicorn.workers.sync import SyncWorker
 from gunicorn import glogging
 from gunicorn.instrument import statsd
@@ -152,6 +153,17 @@ def func(a, b):
     pytest.raises(TypeError, c.set, "pre_fork", lambda x: True)
 
 
+def test_reload_engine_validation():
+    c = config.Config()
+
+    assert c.reload_engine == "auto"
+
+    c.set('reload_engine', 'poll')
+    assert c.reload_engine == 'poll'
+
+    pytest.raises(ConfigError, c.set, "reload_engine", "invalid")
+
+
 def test_callable_validation_for_string():
     from os.path import isdir as testfunc
     assert config.validate_callable(-1)("os.path.isdir") == testfunc