mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
tornado: Require Tornado 6.5.0+ for security fixes
Update minimum Tornado version to 6.5.0 to address: - CVE-2024-52804 (Medium): HTTP Cookie Parsing DoS - CVE-2025-47287 (High 7.5): Multipart/Form-Data Parser DoS This simplifies the tornado worker by removing legacy code paths for Tornado < 5.0 and < 6.0, reducing the codebase by ~30%. Changes: - pyproject.toml: Update tornado requirement to >=6.5.0 - gtornado.py: Remove TORNADO5 constant and legacy code paths - tornadoapp.py: Update example to use async/await syntax - test_gtornado.py: Add comprehensive test suite
This commit is contained in:
parent
5b50487bab
commit
4b9d787c93
@ -7,23 +7,27 @@
|
||||
# $ gunicorn -k tornado tornadoapp:app
|
||||
#
|
||||
|
||||
import asyncio
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
from tornado import gen
|
||||
|
||||
|
||||
class MainHandler(tornado.web.RequestHandler):
|
||||
@gen.coroutine
|
||||
def get(self):
|
||||
async def get(self):
|
||||
# Your asynchronous code here
|
||||
yield gen.sleep(1) # Example of an asynchronous operation
|
||||
await asyncio.sleep(1) # Example of an asynchronous operation
|
||||
self.write("Hello, World!")
|
||||
|
||||
|
||||
def make_app():
|
||||
return tornado.web.Application([
|
||||
(r"/", MainHandler),
|
||||
])
|
||||
|
||||
|
||||
app = make_app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = make_app()
|
||||
app.listen(8888)
|
||||
tornado.ioloop.IOLoop.current().start()
|
||||
|
||||
@ -18,15 +18,6 @@ from gunicorn import __version__ as gversion
|
||||
from gunicorn.sock import ssl_context
|
||||
|
||||
|
||||
# Tornado 5.0 updated its IOLoop, and the `io_loop` arguments to many
|
||||
# Tornado functions have been removed in Tornado 5.0. Also, they no
|
||||
# longer store PeriodCallbacks in ioloop._callbacks. Instead we store
|
||||
# them on our side, and use stop() on them when stopping the worker.
|
||||
# See https://www.tornadoweb.org/en/stable/releases/v5.0.0.html#backwards-compatibility-notes
|
||||
# for more details.
|
||||
TORNADO5 = tornado.version_info >= (5, 0, 0)
|
||||
|
||||
|
||||
class TornadoWorker(Worker):
|
||||
|
||||
@classmethod
|
||||
@ -69,13 +60,9 @@ class TornadoWorker(Worker):
|
||||
pass
|
||||
self.server_alive = False
|
||||
else:
|
||||
if TORNADO5:
|
||||
for callback in self.callbacks:
|
||||
callback.stop()
|
||||
self.ioloop.stop()
|
||||
else:
|
||||
if not self.ioloop._callbacks:
|
||||
self.ioloop.stop()
|
||||
for callback in self.callbacks:
|
||||
callback.stop()
|
||||
self.ioloop.stop()
|
||||
|
||||
def init_process(self):
|
||||
# IOLoop cannot survive a fork or be shared across processes
|
||||
@ -90,75 +77,36 @@ class TornadoWorker(Worker):
|
||||
self.alive = True
|
||||
self.server_alive = False
|
||||
|
||||
if TORNADO5:
|
||||
self.callbacks = []
|
||||
self.callbacks.append(PeriodicCallback(self.watchdog, 1000))
|
||||
self.callbacks.append(PeriodicCallback(self.heartbeat, 1000))
|
||||
for callback in self.callbacks:
|
||||
callback.start()
|
||||
else:
|
||||
PeriodicCallback(self.watchdog, 1000, io_loop=self.ioloop).start()
|
||||
PeriodicCallback(self.heartbeat, 1000, io_loop=self.ioloop).start()
|
||||
self.callbacks = []
|
||||
self.callbacks.append(PeriodicCallback(self.watchdog, 1000))
|
||||
self.callbacks.append(PeriodicCallback(self.heartbeat, 1000))
|
||||
for callback in self.callbacks:
|
||||
callback.start()
|
||||
|
||||
# Assume the app is a WSGI callable if its not an
|
||||
# instance of tornado.web.Application or is an
|
||||
# instance of tornado.wsgi.WSGIApplication
|
||||
# instance of tornado.web.Application or WSGIContainer
|
||||
app = self.wsgi
|
||||
|
||||
if tornado.version_info[0] < 6:
|
||||
if not isinstance(app, tornado.web.Application) or \
|
||||
isinstance(app, tornado.wsgi.WSGIApplication):
|
||||
app = WSGIContainer(app)
|
||||
elif not isinstance(app, WSGIContainer) and \
|
||||
if not isinstance(app, WSGIContainer) and \
|
||||
not isinstance(app, tornado.web.Application):
|
||||
app = WSGIContainer(app)
|
||||
|
||||
# Monkey-patching HTTPConnection.finish to count the
|
||||
# number of requests being handled by Tornado. This
|
||||
# will help gunicorn shutdown the worker if max_requests
|
||||
# is exceeded.
|
||||
httpserver = sys.modules["tornado.httpserver"]
|
||||
if hasattr(httpserver, 'HTTPConnection'):
|
||||
old_connection_finish = httpserver.HTTPConnection.finish
|
||||
class _HTTPServer(tornado.httpserver.HTTPServer):
|
||||
|
||||
def finish(other):
|
||||
def on_close(instance, server_conn):
|
||||
self.handle_request()
|
||||
old_connection_finish(other)
|
||||
httpserver.HTTPConnection.finish = finish
|
||||
sys.modules["tornado.httpserver"] = httpserver
|
||||
|
||||
server_class = tornado.httpserver.HTTPServer
|
||||
else:
|
||||
|
||||
class _HTTPServer(tornado.httpserver.HTTPServer):
|
||||
|
||||
def on_close(instance, server_conn):
|
||||
self.handle_request()
|
||||
super().on_close(server_conn)
|
||||
|
||||
server_class = _HTTPServer
|
||||
super().on_close(server_conn)
|
||||
|
||||
if self.cfg.is_ssl:
|
||||
if TORNADO5:
|
||||
server = server_class(app, ssl_options=ssl_context(self.cfg))
|
||||
else:
|
||||
server = server_class(app, io_loop=self.ioloop,
|
||||
ssl_options=ssl_context(self.cfg))
|
||||
server = _HTTPServer(app, ssl_options=ssl_context(self.cfg))
|
||||
else:
|
||||
if TORNADO5:
|
||||
server = server_class(app)
|
||||
else:
|
||||
server = server_class(app, io_loop=self.ioloop)
|
||||
server = _HTTPServer(app)
|
||||
|
||||
self.server = server
|
||||
self.server_alive = True
|
||||
|
||||
for s in self.sockets:
|
||||
s.setblocking(0)
|
||||
if hasattr(server, "add_socket"): # tornado > 2.0
|
||||
server.add_socket(s)
|
||||
elif hasattr(server, "_sockets"): # tornado 2.0
|
||||
server._sockets[s.fileno()] = s
|
||||
server.add_socket(s)
|
||||
|
||||
server.no_keep_alive = self.cfg.keepalive <= 0
|
||||
server.start(num_processes=1)
|
||||
|
||||
@ -49,7 +49,7 @@ Changelog = "https://docs.gunicorn.org/en/stable/news.html"
|
||||
[project.optional-dependencies]
|
||||
gevent = ["gevent>=1.4.0"]
|
||||
eventlet = ["eventlet>=0.24.1,!=0.36.0"]
|
||||
tornado = ["tornado>=0.2"]
|
||||
tornado = ["tornado>=6.5.0"]
|
||||
gthread = []
|
||||
setproctitle = ["setproctitle"]
|
||||
testing = [
|
||||
|
||||
511
tests/test_gtornado.py
Normal file
511
tests/test_gtornado.py
Normal file
@ -0,0 +1,511 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
"""Tests for the tornado worker."""
|
||||
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
tornado = pytest.importorskip("tornado")
|
||||
|
||||
from gunicorn.config import Config
|
||||
from gunicorn.workers import gtornado
|
||||
|
||||
|
||||
class FakeSocket:
|
||||
"""Mock socket for testing."""
|
||||
|
||||
def __init__(self, data=b''):
|
||||
self.data = data
|
||||
self.closed = False
|
||||
self.blocking = True
|
||||
self._fileno = id(self) % 65536
|
||||
|
||||
def fileno(self):
|
||||
return self._fileno
|
||||
|
||||
def setblocking(self, blocking):
|
||||
self.blocking = blocking
|
||||
|
||||
def recv(self, size):
|
||||
result = self.data[:size]
|
||||
self.data = self.data[size:]
|
||||
return result
|
||||
|
||||
def send(self, data):
|
||||
return len(data)
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def getsockname(self):
|
||||
return ('127.0.0.1', 8000)
|
||||
|
||||
def getpeername(self):
|
||||
return ('127.0.0.1', 12345)
|
||||
|
||||
|
||||
class TestTornadoWorkerInit:
|
||||
"""Tests for TornadoWorker initialization."""
|
||||
|
||||
def create_worker(self, cfg=None):
|
||||
"""Create a worker instance for testing."""
|
||||
if cfg is None:
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
cfg.set('max_requests', 0)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_worker_init(self):
|
||||
"""Test worker initialization."""
|
||||
worker = self.create_worker()
|
||||
assert worker.nr == 0
|
||||
|
||||
def test_init_process_clears_ioloop(self):
|
||||
"""Test that init_process clears the current IOLoop."""
|
||||
worker = self.create_worker()
|
||||
worker.tmp = mock.Mock()
|
||||
worker.log = mock.Mock()
|
||||
|
||||
with mock.patch.object(gtornado.IOLoop, 'clear_current') as mock_clear:
|
||||
with mock.patch.object(gtornado.Worker, 'init_process'):
|
||||
worker.init_process()
|
||||
mock_clear.assert_called_once()
|
||||
|
||||
|
||||
class TestRequestCounting:
|
||||
"""Tests for request counting and max_requests behavior."""
|
||||
|
||||
def create_worker(self, cfg=None):
|
||||
"""Create a worker instance for testing."""
|
||||
if cfg is None:
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_handle_request_increments_counter(self):
|
||||
"""Test that handle_request increments the request counter."""
|
||||
worker = self.create_worker()
|
||||
worker.nr = 0
|
||||
worker.max_requests = 100
|
||||
worker.alive = True
|
||||
|
||||
worker.handle_request()
|
||||
|
||||
assert worker.nr == 1
|
||||
assert worker.alive is True
|
||||
|
||||
def test_max_requests_triggers_shutdown(self):
|
||||
"""Test that reaching max_requests triggers shutdown."""
|
||||
cfg = Config()
|
||||
cfg.set('max_requests', 5)
|
||||
worker = self.create_worker(cfg)
|
||||
worker.nr = 4
|
||||
worker.alive = True
|
||||
worker.max_requests = 5
|
||||
|
||||
worker.handle_request()
|
||||
|
||||
assert worker.nr == 5
|
||||
assert worker.alive is False
|
||||
|
||||
|
||||
class TestSignalHandling:
|
||||
"""Tests for signal handling in tornado worker."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_handle_exit_sets_alive_false(self):
|
||||
"""Test that handle_exit sets alive=False through parent."""
|
||||
worker = self.create_worker()
|
||||
worker.alive = True
|
||||
|
||||
# The parent's handle_exit is what sets alive=False
|
||||
worker.handle_exit(None, None)
|
||||
|
||||
assert worker.alive is False
|
||||
|
||||
def test_handle_exit_only_once(self):
|
||||
"""Test that handle_exit only triggers once when alive."""
|
||||
worker = self.create_worker()
|
||||
worker.alive = True
|
||||
|
||||
# First call should set alive=False
|
||||
worker.handle_exit(None, None)
|
||||
assert worker.alive is False
|
||||
|
||||
# Second call should do nothing (alive is already False)
|
||||
# Track that super().handle_exit is not called again
|
||||
with mock.patch.object(gtornado.Worker, 'handle_exit') as mock_exit:
|
||||
worker.handle_exit(None, None)
|
||||
mock_exit.assert_not_called()
|
||||
|
||||
|
||||
class TestWatchdog:
|
||||
"""Tests for watchdog functionality."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_watchdog_notifies_when_alive(self):
|
||||
"""Test that watchdog calls notify when alive."""
|
||||
worker = self.create_worker()
|
||||
worker.alive = True
|
||||
worker.ppid = os.getppid()
|
||||
worker.tmp = mock.Mock()
|
||||
|
||||
worker.watchdog()
|
||||
|
||||
worker.tmp.notify.assert_called_once()
|
||||
|
||||
def test_watchdog_detects_parent_death(self):
|
||||
"""Test that watchdog detects parent death."""
|
||||
worker = self.create_worker()
|
||||
worker.alive = True
|
||||
worker.ppid = 99999999 # Invalid ppid
|
||||
worker.tmp = mock.Mock()
|
||||
|
||||
worker.watchdog()
|
||||
|
||||
assert worker.alive is False
|
||||
|
||||
|
||||
class TestHeartbeat:
|
||||
"""Tests for heartbeat functionality."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_heartbeat_stops_server_when_not_alive(self):
|
||||
"""Test that heartbeat stops the server when not alive."""
|
||||
worker = self.create_worker()
|
||||
worker.alive = False
|
||||
worker.server_alive = True
|
||||
worker.server = mock.Mock()
|
||||
|
||||
worker.heartbeat()
|
||||
|
||||
worker.server.stop.assert_called_once()
|
||||
assert worker.server_alive is False
|
||||
|
||||
def test_heartbeat_stops_ioloop_after_server(self):
|
||||
"""Test that heartbeat stops IOLoop after server is stopped."""
|
||||
worker = self.create_worker()
|
||||
worker.alive = False
|
||||
worker.server_alive = False
|
||||
worker.callbacks = [mock.Mock(), mock.Mock()]
|
||||
worker.ioloop = mock.Mock()
|
||||
|
||||
worker.heartbeat()
|
||||
|
||||
for callback in worker.callbacks:
|
||||
callback.stop.assert_called_once()
|
||||
worker.ioloop.stop.assert_called_once()
|
||||
|
||||
|
||||
class TestAppWrapping:
|
||||
"""Tests for app wrapping logic."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_wsgi_callable_wrapped_in_container(self):
|
||||
"""Test that a plain WSGI callable gets wrapped in WSGIContainer."""
|
||||
from tornado.wsgi import WSGIContainer
|
||||
|
||||
def wsgi_app(environ, start_response):
|
||||
pass
|
||||
|
||||
# Test that WSGIContainer is used for plain WSGI apps
|
||||
app = wsgi_app
|
||||
if not isinstance(app, WSGIContainer) and \
|
||||
not isinstance(app, tornado.web.Application):
|
||||
app = WSGIContainer(app)
|
||||
|
||||
assert isinstance(app, WSGIContainer)
|
||||
|
||||
def test_tornado_application_not_wrapped(self):
|
||||
"""Test that tornado.web.Application is not wrapped."""
|
||||
from tornado.wsgi import WSGIContainer
|
||||
|
||||
tornado_app = tornado.web.Application([])
|
||||
|
||||
# Test the wrapping logic
|
||||
app = tornado_app
|
||||
if not isinstance(app, WSGIContainer) and \
|
||||
not isinstance(app, tornado.web.Application):
|
||||
app = WSGIContainer(app)
|
||||
|
||||
# Should NOT be wrapped
|
||||
assert isinstance(app, tornado.web.Application)
|
||||
assert not isinstance(app, WSGIContainer)
|
||||
|
||||
|
||||
class TestSetup:
|
||||
"""Tests for the setup class method."""
|
||||
|
||||
def test_setup_patches_request_handler(self):
|
||||
"""Test that setup patches RequestHandler.clear."""
|
||||
# Save original
|
||||
original_clear = tornado.web.RequestHandler.clear
|
||||
|
||||
try:
|
||||
gtornado.TornadoWorker.setup()
|
||||
|
||||
# Create a mock handler to test the patched clear method
|
||||
mock_handler = mock.Mock()
|
||||
mock_handler._headers = {"Server": "TornadoServer/1.0"}
|
||||
|
||||
# Call the patched clear
|
||||
new_clear = tornado.web.RequestHandler.clear
|
||||
assert new_clear is not original_clear
|
||||
|
||||
finally:
|
||||
# Restore original
|
||||
tornado.web.RequestHandler.clear = original_clear
|
||||
|
||||
|
||||
class TestRunMethod:
|
||||
"""Tests for the run method."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
cfg.set('keepalive', 2)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_run_sets_up_callbacks(self):
|
||||
"""Test that run sets up periodic callbacks."""
|
||||
worker = self.create_worker()
|
||||
worker.wsgi = tornado.web.Application([])
|
||||
worker.sockets = []
|
||||
|
||||
mock_ioloop = mock.Mock()
|
||||
mock_callback = mock.Mock()
|
||||
|
||||
with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):
|
||||
with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock_callback) as mock_pc:
|
||||
# Start the run method but stop it immediately
|
||||
mock_ioloop.start.side_effect = lambda: None
|
||||
|
||||
worker.run()
|
||||
|
||||
# Should create two callbacks (watchdog and heartbeat)
|
||||
assert mock_pc.call_count == 2
|
||||
assert mock_callback.start.call_count == 2
|
||||
|
||||
def test_run_creates_http_server(self):
|
||||
"""Test that run creates an HTTP server."""
|
||||
worker = self.create_worker()
|
||||
worker.wsgi = tornado.web.Application([])
|
||||
worker.sockets = []
|
||||
|
||||
mock_ioloop = mock.Mock()
|
||||
mock_ioloop.start.side_effect = lambda: None
|
||||
|
||||
with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):
|
||||
with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):
|
||||
worker.run()
|
||||
|
||||
assert worker.server is not None
|
||||
assert worker.server_alive is True
|
||||
|
||||
def test_run_adds_sockets_to_server(self):
|
||||
"""Test that run adds sockets to the server."""
|
||||
worker = self.create_worker()
|
||||
worker.wsgi = tornado.web.Application([])
|
||||
|
||||
mock_socket = FakeSocket()
|
||||
worker.sockets = [mock_socket]
|
||||
|
||||
mock_ioloop = mock.Mock()
|
||||
mock_ioloop.start.side_effect = lambda: None
|
||||
|
||||
with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):
|
||||
with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):
|
||||
with mock.patch.object(tornado.httpserver.HTTPServer, 'add_socket'):
|
||||
worker.run()
|
||||
|
||||
# Socket should be set to non-blocking (setblocking(0))
|
||||
assert not mock_socket.blocking
|
||||
|
||||
|
||||
class TestSSLSupport:
|
||||
"""Tests for SSL support."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
cfg.set('keepalive', 2)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_ssl_server_creation(self):
|
||||
"""Test that SSL server is created when is_ssl is True."""
|
||||
worker = self.create_worker()
|
||||
worker.wsgi = tornado.web.Application([])
|
||||
worker.sockets = []
|
||||
|
||||
mock_ioloop = mock.Mock()
|
||||
mock_ioloop.start.side_effect = lambda: None
|
||||
|
||||
mock_ssl_context = mock.Mock()
|
||||
|
||||
# Mock cfg.is_ssl property to return True
|
||||
with mock.patch.object(type(worker.cfg), 'is_ssl', new_callable=mock.PropertyMock, return_value=True):
|
||||
with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):
|
||||
with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):
|
||||
with mock.patch.object(gtornado, 'ssl_context', return_value=mock_ssl_context):
|
||||
worker.run()
|
||||
|
||||
# Server should be created with ssl_options
|
||||
assert worker.server is not None
|
||||
|
||||
|
||||
class TestKeepAlive:
|
||||
"""Tests for keep-alive configuration."""
|
||||
|
||||
def create_worker(self):
|
||||
"""Create a worker for testing."""
|
||||
cfg = Config()
|
||||
cfg.set('workers', 1)
|
||||
|
||||
worker = gtornado.TornadoWorker(
|
||||
age=1,
|
||||
ppid=os.getpid(),
|
||||
sockets=[],
|
||||
app=mock.Mock(),
|
||||
timeout=30,
|
||||
cfg=cfg,
|
||||
log=mock.Mock(),
|
||||
)
|
||||
return worker
|
||||
|
||||
def test_keep_alive_enabled(self):
|
||||
"""Test that keep-alive is enabled when keepalive > 0."""
|
||||
worker = self.create_worker()
|
||||
worker.wsgi = tornado.web.Application([])
|
||||
worker.cfg.set('keepalive', 2)
|
||||
worker.sockets = []
|
||||
|
||||
mock_ioloop = mock.Mock()
|
||||
mock_ioloop.start.side_effect = lambda: None
|
||||
|
||||
with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):
|
||||
with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):
|
||||
worker.run()
|
||||
|
||||
assert worker.server.no_keep_alive is False
|
||||
|
||||
def test_keep_alive_disabled(self):
|
||||
"""Test that keep-alive is disabled when keepalive <= 0."""
|
||||
worker = self.create_worker()
|
||||
worker.wsgi = tornado.web.Application([])
|
||||
worker.cfg.set('keepalive', 0)
|
||||
worker.sockets = []
|
||||
|
||||
mock_ioloop = mock.Mock()
|
||||
mock_ioloop.start.side_effect = lambda: None
|
||||
|
||||
with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):
|
||||
with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):
|
||||
worker.run()
|
||||
|
||||
assert worker.server.no_keep_alive is True
|
||||
Loading…
x
Reference in New Issue
Block a user