diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 046770616..d5a7db3c9 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -1758,6 +1758,22 @@ For the non sync workers it just means that the worker process is still communicating and is not tied to the length of time required to handle a single request. +.. _timeout-delay: + +``timeout_delay`` +~~~~~~~~~~~~~~~~~ + +**Command line:** ``--timeout-delay INT`` + +**Default:** ``0`` + +Workers are allowed this much time to start up before the timeout period +is enforced. If the worker is active before this delay expires, the +delay period is cancelled and timeout checks begin immediately. + +Value is a positive number or 0. Setting it to 0 has the effect of +no delay period before timeout checks are enforced. + .. _graceful-timeout: ``graceful_timeout`` diff --git a/gunicorn/config.py b/gunicorn/config.py index 29b30ad23..c3fcb9ce5 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -798,6 +798,24 @@ class Timeout(Setting): """ +class TimeoutDelay(Setting): + name = "timeout_delay" + section = "Worker Processes" + cli = ["--timeout-delay"] + meta = "INT" + validator = validate_pos_int + type = int + default = 0 + desc = """\ + Workers are allowed this much time to start up before the timeout period + is enforced. If the worker is active before this delay expires, the + delay period is cancelled and timeout checks begin immediately. + + Value is a positive number or 0. Setting it to 0 has the effect of + no delay period before timeout checks are enforced. + """ + + class GracefulTimeout(Setting): name = "graceful_timeout" section = "Worker Processes" diff --git a/gunicorn/workers/workertmp.py b/gunicorn/workers/workertmp.py index 8ef00a560..98ec3da77 100644 --- a/gunicorn/workers/workertmp.py +++ b/gunicorn/workers/workertmp.py @@ -39,10 +39,16 @@ def __init__(self, cfg): os.close(fd) raise - def notify(self): - new_time = time.monotonic() + # set the file times in the future if a delay is configured + self._set_time(delay=cfg.timeout_delay) + + def _set_time(self, delay=0): + new_time = time.monotonic() + delay os.utime(self._tmp.fileno(), (new_time, new_time)) + def notify(self): + self._set_time() + def last_update(self): return os.fstat(self._tmp.fileno()).st_mtime diff --git a/tests/workers/test_workertmp.py b/tests/workers/test_workertmp.py new file mode 100644 index 000000000..0d4805e5b --- /dev/null +++ b/tests/workers/test_workertmp.py @@ -0,0 +1,75 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import io +from unittest import mock + +import pytest + +from gunicorn import config +from gunicorn.workers.workertmp import WorkerTmp + + +@pytest.fixture +def cfg(tmp_path): + c = config.Config() + c.set('worker_tmp_dir', str(tmp_path)) + return c + + +@mock.patch('time.monotonic') +def test_creation(mock_monotonic, cfg): + mock_monotonic.side_effect = [100.0] + wt = WorkerTmp(cfg) + + mock_monotonic.assert_called_once() + assert isinstance(wt._tmp, io.IOBase) + assert wt.last_update() == 100.0 + + +@mock.patch('time.monotonic') +def test_creation_with_delay(mock_monotonic, cfg): + mock_monotonic.side_effect = [100.0] + cfg.set('timeout_delay', 50) + wt = WorkerTmp(cfg) + + mock_monotonic.assert_called_once() + assert isinstance(wt._tmp, io.IOBase) + assert wt.last_update() == 150.0 + + +@mock.patch('time.monotonic') +def test_notify(mock_monotonic, cfg): + mock_monotonic.side_effect = [100.0, 200.0] + wt = WorkerTmp(cfg) + wt.notify() + + mock_monotonic.assert_has_calls([(), ()]) + assert wt.last_update() == 200.0 + + +@mock.patch('time.monotonic') +def test_notify_before_delay(mock_monotonic, cfg): + mock_monotonic.side_effect = [100.0, 200.0] + cfg.set('timeout_delay', 300) + + wt = WorkerTmp(cfg) + assert wt.last_update() == 400.0 + wt.notify() + + mock_monotonic.assert_has_calls([(), ()]) + assert wt.last_update() == 200.0 + + +@mock.patch('time.monotonic') +def test_notify_after_delay(mock_monotonic, cfg): + mock_monotonic.side_effect = [100.0, 500.0] + cfg.set('timeout_delay', 300) + + wt = WorkerTmp(cfg) + assert wt.last_update() == 400.0 + wt.notify() + + mock_monotonic.assert_has_calls([(), ()]) + assert wt.last_update() == 500.0