From 201df19a8011c0a1d6a0e75ebe22e89d48eb935e Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Tue, 5 May 2026 00:36:46 +0200 Subject: [PATCH] chore: remove eventlet worker; add h2 and uvloop to test deps Eventlet was deprecated for 26.0 and is now removed: - Delete gunicorn/workers/geventlet.py and its registry entry - Drop eventlet from config help text, HTTP/2 unsupported-worker messages, and the dirty client docstring - Drop the eventlet optional-dependency, the eventlet entry in the testing extra, and the eventlet-only filterwarnings ignore - Drop the EventletWorkerAlpn test class - Drop the freebsd CI ignore for the (now non-existent) test_geventlet.py - Drop eventlet from the issue-triage discussion template - Drop eventlet from README, install/design/http2/settings/news docs; rewrite the news.md entry from 'deprecated' to 'removed in this release' Add h2 and uvloop to requirements_test.txt so a plain 'pip install -r requirements_test.txt' run reaches feature parity with 'pip install .[testing]' for those two deps. The container suite previously skipped 87 HTTP/2 tests for missing h2 and 1 for uvloop; the in-process suite skips drop from 67 to 40. --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 1 - .github/workflows/freebsd.yml | 3 +- README.md | 2 +- docs/content/design.md | 18 -- docs/content/guides/http2.md | 1 - docs/content/install.md | 1 - docs/content/news.md | 12 +- docs/content/reference/settings.md | 3 +- gunicorn/config.py | 3 +- gunicorn/dirty/client.py | 2 +- gunicorn/workers/__init__.py | 1 - gunicorn/workers/geventlet.py | 217 ------------------- gunicorn/workers/gtornado.py | 2 +- gunicorn/workers/sync.py | 2 +- pyproject.toml | 7 - requirements_test.txt | 2 + tests/test_http2_alpn.py | 75 +------ 17 files changed, 16 insertions(+), 336 deletions(-) delete mode 100644 gunicorn/workers/geventlet.py diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 36d89810..18c2515a 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -95,7 +95,6 @@ body: - sync (default) - gthread - gevent - - eventlet - tornado - asgi (beta) - custom diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 120cc909..155177d9 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -43,5 +43,4 @@ jobs: pip install pytest pytest-cov pytest-asyncio coverage pip install -e . pytest --cov=gunicorn -v tests/ \ - --ignore=tests/workers/test_ggevent.py \ - --ignore=tests/workers/test_geventlet.py + --ignore=tests/workers/test_ggevent.py diff --git a/README.md b/README.md index 7af33393..8f46f86b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ gunicorn myapp:app --worker-class asgi - **HTTP/2 support** (beta) with multiplexed streams - **Dirty Arbiters** (beta) for heavy workloads (ML models, long-running tasks) - uWSGI binary protocol for nginx integration -- Multiple worker types: sync, gthread, gevent, eventlet, asgi +- Multiple worker types: sync, gthread, gevent, asgi - Graceful worker process management - Compatible with Python 3.9+ diff --git a/docs/content/design.md b/docs/content/design.md index c06c1cbd..112de02d 100644 --- a/docs/content/design.md +++ b/docs/content/design.md @@ -95,23 +95,6 @@ Choose a worker type based on your application's needs. gunicorn myapp:app -k gevent --worker-connections 1000 ``` -=== "Eventlet (Deprecated)" - - !!! warning "Deprecated" - The eventlet worker is **deprecated** and will be removed in Gunicorn 26.0. - Eventlet itself is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html). - Please migrate to `gevent`, `gthread`, or another supported worker type. - - **Greenlet-based** async worker using [Eventlet](http://eventlet.net/). - - - Similar capabilities to Gevent - - Handles high concurrency for I/O-bound apps - - Some libraries may need compatibility patches - - ```bash - gunicorn myapp:app -k eventlet --worker-connections 1000 - ``` - === "Tornado" Worker for [Tornado](https://www.tornadoweb.org/) applications. @@ -132,7 +115,6 @@ Choose a worker type based on your application's needs. | `gthread` | Thread pool | ✅ | Mixed workloads, moderate concurrency | | ASGI workers | AsyncIO | ✅ | Modern async frameworks (FastAPI, etc.) | | `gevent` | Greenlets | ✅ | I/O-bound, WebSockets, streaming | -| `eventlet` | Greenlets | ✅ | **Deprecated** - use `gevent` instead | | `tornado` | Tornado IOLoop | ✅ | Native Tornado applications | !!! tip "Quick Decision Guide" diff --git a/docs/content/guides/http2.md b/docs/content/guides/http2.md index d187b1de..1c690ae2 100644 --- a/docs/content/guides/http2.md +++ b/docs/content/guides/http2.md @@ -109,7 +109,6 @@ Not all workers support HTTP/2: | `sync` | No | Single-threaded, cannot multiplex streams | | `gthread` | Yes | Recommended for HTTP/2 | | `gevent` | Yes | Requires gevent | -| `eventlet` | Yes | **Deprecated** - will be removed in 26.0 | | `asgi` | Yes | For async frameworks | | `tornado` | No | Tornado handles its own protocol | diff --git a/docs/content/install.md b/docs/content/install.md index 1804bb5e..a74d16e0 100644 --- a/docs/content/install.md +++ b/docs/content/install.md @@ -94,7 +94,6 @@ pip install gunicorn[gevent,setproctitle] | `gunicorn[gevent]` | Gevent-based greenlet workers | | `gunicorn[gthread]` | Threaded workers | | `gunicorn[tornado]` | Tornado-based workers (not recommended) | -| `gunicorn[eventlet]` | **Deprecated** - will be removed in 26.0 | See the [design docs](design.md) for guidance on choosing worker types. diff --git a/docs/content/news.md b/docs/content/news.md index 3caa9ebc..e5ef7275 100644 --- a/docs/content/news.md +++ b/docs/content/news.md @@ -230,7 +230,7 @@ ### Bug Fixes -- Fix HTTP/2 ALPN negotiation for gevent and eventlet workers when +- Fix HTTP/2 ALPN negotiation for the gevent worker when `do_handshake_on_connect` is False (the default). The TLS handshake is now explicitly performed before checking `selected_alpn_protocol()`. @@ -250,11 +250,12 @@ - Fix ASGI: quick shutdown on SIGINT/SIGQUIT, graceful on SIGTERM -### Deprecations +### Removals -- **Eventlet Worker**: The `eventlet` worker is deprecated and will be removed in - Gunicorn 26.0. Eventlet itself is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html). - Please migrate to `gevent`, `gthread`, or another supported worker type. +- **Eventlet Worker**: The `eventlet` worker has been removed. Eventlet itself + is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html); + the worker was deprecated in 25.x and is now gone. Migrate to `gevent`, + `gthread`, or one of the ASGI workers. ### Changes @@ -329,7 +330,6 @@ ### Security -- **eventlet**: Require eventlet >= 0.40.3 (CVE-2021-21419, CVE-2025-58068) - **gevent**: Require gevent >= 24.10.1 (CVE-2023-41419, CVE-2024-3219) - **tornado**: Require tornado >= 6.5.0 (CVE-2025-47287) diff --git a/docs/content/reference/settings.md b/docs/content/reference/settings.md index fc2474c5..c7d54265 100644 --- a/docs/content/reference/settings.md +++ b/docs/content/reference/settings.md @@ -1793,7 +1793,6 @@ libraries may be installed using setuptools' ``extras_require`` feature. A string referring to one of the following bundled classes: * ``sync`` -* ``eventlet`` - **DEPRECATED: will be removed in 26.0**. Requires eventlet >= 0.40.3 * ``gevent`` - Requires gevent >= 24.10.1 (or install it via ``pip install gunicorn[gevent]``) * ``tornado`` - Requires tornado >= 6.5.0 (or install it via @@ -1837,7 +1836,7 @@ This setting only affects the Gthread worker type. The maximum number of simultaneous clients. -This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types. +This setting only affects the ``gthread`` and ``gevent`` worker types. ### `max_requests` diff --git a/gunicorn/config.py b/gunicorn/config.py index e3f37913..c6ab3777 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -719,7 +719,6 @@ class WorkerClass(Setting): A string referring to one of the following bundled classes: * ``sync`` - * ``eventlet`` - **DEPRECATED: will be removed in 26.0**. Requires eventlet >= 0.40.3 * ``gevent`` - Requires gevent >= 24.10.1 (or install it via ``pip install gunicorn[gevent]``) * ``tornado`` - Requires tornado >= 6.5.0 (or install it via @@ -773,7 +772,7 @@ class WorkerConnections(Setting): desc = """\ The maximum number of simultaneous clients. - This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types. + This setting only affects the ``gthread`` and ``gevent`` worker types. """ diff --git a/gunicorn/dirty/client.py b/gunicorn/dirty/client.py index 2a38cc8a..c5cc31cd 100644 --- a/gunicorn/dirty/client.py +++ b/gunicorn/dirty/client.py @@ -34,7 +34,7 @@ class DirtyClient: Provides both sync and async APIs. The sync API is for traditional sync workers (sync, gthread), while the async API is for async - workers (asgi, gevent, eventlet). + workers (asgi, gevent). """ def __init__(self, socket_path, timeout=30.0): diff --git a/gunicorn/workers/__init__.py b/gunicorn/workers/__init__.py index ad77416d..ce0f8f51 100644 --- a/gunicorn/workers/__init__.py +++ b/gunicorn/workers/__init__.py @@ -5,7 +5,6 @@ # supported gunicorn workers. SUPPORTED_WORKERS = { "sync": "gunicorn.workers.sync.SyncWorker", - "eventlet": "gunicorn.workers.geventlet.EventletWorker", # DEPRECATED: will be removed in 26.0 "gevent": "gunicorn.workers.ggevent.GeventWorker", "gevent_wsgi": "gunicorn.workers.ggevent.GeventPyWSGIWorker", "gevent_pywsgi": "gunicorn.workers.ggevent.GeventPyWSGIWorker", diff --git a/gunicorn/workers/geventlet.py b/gunicorn/workers/geventlet.py deleted file mode 100644 index a99d416c..00000000 --- a/gunicorn/workers/geventlet.py +++ /dev/null @@ -1,217 +0,0 @@ -# -# This file is part of gunicorn released under the MIT license. -# See the NOTICE for more information. - -# DEPRECATION NOTICE: The eventlet worker is deprecated and will be removed -# in Gunicorn 26.0. Eventlet itself is deprecated and no longer maintained. -# Please migrate to gevent, gthread, or another supported worker type. -# See: https://eventlet.readthedocs.io/en/latest/asyncio/migration.html - -import warnings - -warnings.warn( - "The eventlet worker is deprecated and will be removed in Gunicorn 26.0. " - "Please migrate to gevent, gthread, or another supported worker type. " - "See: https://docs.gunicorn.org/en/stable/design.html#choosing-a-worker-type", - DeprecationWarning, - stacklevel=2 -) - -# NOTE: eventlet import and monkey_patch() must happen before any other imports -# to ensure all standard library modules are properly patched. -try: - import eventlet -except ImportError: - raise RuntimeError("eventlet worker requires eventlet 0.40.3 or higher") -else: - from packaging.version import parse as parse_version - if parse_version(eventlet.__version__) < parse_version('0.40.3'): - raise RuntimeError("eventlet worker requires eventlet 0.40.3 or higher") - -# Perform monkey patching early, before importing other modules. -# This ensures that all subsequent imports get the patched versions. -# NOTE: hubs.use_hub() must NOT be called here - it creates OS resources -# (like kqueue on macOS) that don't survive fork. It must be called in -# each worker process after fork, in the patch() method. -eventlet.monkey_patch() - -from functools import partial # noqa: E402 -import sys # noqa: E402 - -from eventlet import hubs, greenthread # noqa: E402 -from eventlet.greenio import GreenSocket # noqa: E402 -import eventlet.wsgi # noqa: E402 -import greenlet # noqa: E402 - -from gunicorn.workers.base_async import AsyncWorker # noqa: E402 -from gunicorn.sock import ssl_wrap_socket # noqa: E402 - -# ALREADY_HANDLED is removed in 0.30.3+ now it's `WSGI_LOCAL.already_handled: bool` -# https://github.com/eventlet/eventlet/pull/544 -EVENTLET_WSGI_LOCAL = getattr(eventlet.wsgi, "WSGI_LOCAL", None) -EVENTLET_ALREADY_HANDLED = getattr(eventlet.wsgi, "ALREADY_HANDLED", None) - - -def _eventlet_socket_sendfile(self, file, offset=0, count=None): - # Based on the implementation in gevent which in turn is slightly - # modified from the standard library implementation. - if self.gettimeout() == 0: - raise ValueError("non-blocking sockets are not supported") - if offset: - file.seek(offset) - blocksize = min(count, 8192) if count else 8192 - total_sent = 0 - # localize variable access to minimize overhead - file_read = file.read - sock_send = self.send - try: - while True: - if count: - blocksize = min(count - total_sent, blocksize) - if blocksize <= 0: - break - data = memoryview(file_read(blocksize)) - if not data: - break # EOF - while True: - try: - sent = sock_send(data) - except BlockingIOError: - continue - else: - total_sent += sent - if sent < len(data): - data = data[sent:] - else: - break - return total_sent - finally: - if total_sent > 0 and hasattr(file, 'seek'): - file.seek(offset + total_sent) - - -def _eventlet_serve(sock, handle, concurrency): - """ - Serve requests forever. - - This code is nearly identical to ``eventlet.convenience.serve`` except - that it attempts to join the pool at the end, which allows for gunicorn - graceful shutdowns. - """ - pool = eventlet.greenpool.GreenPool(concurrency) - server_gt = eventlet.greenthread.getcurrent() - - while True: - try: - conn, addr = sock.accept() - gt = pool.spawn(handle, conn, addr) - gt.link(_eventlet_stop, server_gt, conn) - conn, addr, gt = None, None, None - except eventlet.StopServe: - sock.close() - pool.waitall() - return - - -def _eventlet_stop(client, server, conn): - """ - Stop a greenlet handling a request and close its connection. - - This code is lifted from eventlet so as not to depend on undocumented - functions in the library. - """ - try: - try: - client.wait() - finally: - conn.close() - except greenlet.GreenletExit: - pass - except Exception: - greenthread.kill(server, *sys.exc_info()) - - -def patch_sendfile(): - # As of eventlet 0.25.1, GreenSocket.sendfile doesn't exist, - # meaning the native implementations of socket.sendfile will be used. - # If os.sendfile exists, it will attempt to use that, failing explicitly - # if the socket is in non-blocking mode, which the underlying - # socket object /is/. Even the regular _sendfile_use_send will - # fail in that way; plus, it would use the underlying socket.send which isn't - # properly cooperative. So we have to monkey-patch a working socket.sendfile() - # into GreenSocket; in this method, `self.send` will be the GreenSocket's - # send method which is properly cooperative. - if not hasattr(GreenSocket, 'sendfile'): - GreenSocket.sendfile = _eventlet_socket_sendfile - - -class EventletWorker(AsyncWorker): - - def patch(self): - # NOTE: eventlet.monkey_patch() is called at module import time to - # ensure all imports are properly patched. However, hubs.use_hub() - # must be called here (after fork) because it creates OS resources - # like kqueue that don't survive fork. - hubs.use_hub() - patch_sendfile() - - def is_already_handled(self, respiter): - # eventlet >= 0.30.3 - if getattr(EVENTLET_WSGI_LOCAL, "already_handled", None): - raise StopIteration() - # eventlet < 0.30.3 - if respiter == EVENTLET_ALREADY_HANDLED: - raise StopIteration() - return super().is_already_handled(respiter) - - def init_process(self): - self.log.warning( - "The eventlet worker is DEPRECATED and will be removed in Gunicorn 26.0. " - "Please migrate to gevent, gthread, or another supported worker type." - ) - self.patch() - super().init_process() - - def handle_quit(self, sig, frame): - eventlet.spawn(super().handle_quit, sig, frame) - - def handle_usr1(self, sig, frame): - eventlet.spawn(super().handle_usr1, sig, frame) - - def timeout_ctx(self): - return eventlet.Timeout(self.cfg.keepalive or None, False) - - def handle(self, listener, client, addr): - if self.cfg.is_ssl: - client = ssl_wrap_socket(client, self.cfg) - super().handle(listener, client, addr) - - def run(self): - acceptors = [] - for sock in self.sockets: - gsock = GreenSocket(sock) - gsock.setblocking(1) - hfun = partial(self.handle, gsock) - acceptor = eventlet.spawn(_eventlet_serve, gsock, hfun, - self.worker_connections) - - acceptors.append(acceptor) - eventlet.sleep(0.0) - - while self.alive: - self.notify() - eventlet.sleep(1.0) - - self.notify() - t = None - try: - with eventlet.Timeout(self.cfg.graceful_timeout) as t: - for a in acceptors: - a.kill(eventlet.StopServe()) - for a in acceptors: - a.wait() - except eventlet.Timeout as te: - if te != t: - raise - for a in acceptors: - a.kill() diff --git a/gunicorn/workers/gtornado.py b/gunicorn/workers/gtornado.py index fc75fc2d..f5fc5dfb 100644 --- a/gunicorn/workers/gtornado.py +++ b/gunicorn/workers/gtornado.py @@ -81,7 +81,7 @@ class TornadoWorker(Worker): if 'h2' in self.cfg.http_protocols: self.log.warning( "HTTP/2 is not supported by the tornado worker. " - "Use gthread, gevent, eventlet, or asgi workers for HTTP/2 support. " + "Use gthread, gevent, or asgi workers for HTTP/2 support. " "Falling back to HTTP/1.1 only." ) diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index dc731060..c11597f2 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -117,7 +117,7 @@ class SyncWorker(base.Worker): if 'h2' in self.cfg.http_protocols: self.log.warning( "HTTP/2 is not supported by the sync worker. " - "Use gthread, gevent, eventlet, or asgi workers for HTTP/2 support. " + "Use gthread, gevent, or asgi workers for HTTP/2 support. " "Falling back to HTTP/1.1 only." ) diff --git a/pyproject.toml b/pyproject.toml index f6c26b4f..eec37395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ Changelog = "https://gunicorn.org/news/" [project.optional-dependencies] gevent = ["gevent>=24.10.1"] -eventlet = ["eventlet>=0.40.3"] tornado = ["tornado>=6.5.0"] gthread = [] setproctitle = ["setproctitle"] @@ -56,7 +55,6 @@ http2 = ["h2>=4.1.0"] fast = ["gunicorn_h1c>=0.6.5"] testing = [ "gevent>=24.10.1", - "eventlet>=0.40.3", "h2>=4.1.0", "coverage", "pytest", @@ -82,11 +80,6 @@ testpaths = ["tests/"] addopts = "--assert=plain --cov=gunicorn --cov-report=xml" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" -filterwarnings = [ - # Eventlet patches select module, which breaks asyncio event loop cleanup - # This is expected behavior when testing eventlet worker - "ignore::pytest.PytestUnraisableExceptionWarning", -] [tool.setuptools] zip-safe = false diff --git a/requirements_test.txt b/requirements_test.txt index b1abc181..6ac192f5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,3 +4,5 @@ pytest>=7.2.0 pytest-cov pytest-asyncio gunicorn_h1c>=0.6.5 +h2>=4.1.0 +uvloop>=0.19.0 diff --git a/tests/test_http2_alpn.py b/tests/test_http2_alpn.py index 360bc226..d5aaab92 100644 --- a/tests/test_http2_alpn.py +++ b/tests/test_http2_alpn.py @@ -211,7 +211,7 @@ class TestAlpnProtocolMap: class TestAsyncWorkerAlpnHandshake: """Test that AsyncWorker performs handshake before ALPN check. - This is critical for gevent and eventlet workers where do_handshake_on_connect + This is critical for the gevent worker where do_handshake_on_connect may be False, causing ALPN negotiation to not complete until first I/O. """ @@ -353,76 +353,3 @@ class TestGeventWorkerAlpn: mock_super.assert_called_once() -class TestEventletWorkerAlpn: - """Test ALPN handling in EventletWorker.""" - - @pytest.fixture - def eventlet_worker(self): - """Create an EventletWorker instance for testing.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import EventletWorker - - worker = EventletWorker.__new__(EventletWorker) - worker.cfg = mock.MagicMock() - worker.cfg.keepalive = 2 - worker.cfg.do_handshake_on_connect = False - worker.cfg.http_protocols = ["h2", "h1"] - worker.cfg.is_ssl = True - worker.alive = True - worker.log = mock.MagicMock() - worker.wsgi = mock.MagicMock() - worker.nr = 0 - worker.max_requests = 1000 - worker.worker_connections = 1000 - - return worker - - def test_eventlet_inherits_async_worker(self): - """Test that EventletWorker inherits from AsyncWorker.""" - try: - import eventlet - except (ImportError, AttributeError): - pytest.skip("eventlet not available") - - from gunicorn.workers.geventlet import EventletWorker - from gunicorn.workers.base_async import AsyncWorker - - assert issubclass(EventletWorker, AsyncWorker) - - def test_eventlet_handle_wraps_ssl_then_calls_super(self, eventlet_worker): - """Test that EventletWorker.handle() wraps SSL then calls super().""" - from gunicorn.workers import geventlet - - mock_client = mock.MagicMock() - mock_wrapped = mock.MagicMock() - mock_listener = mock.MagicMock() - - with mock.patch.object(geventlet, 'ssl_wrap_socket', return_value=mock_wrapped): - with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle') as mock_super: - eventlet_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000)) - - # Verify super().handle() was called with the wrapped socket - mock_super.assert_called_once() - call_args = mock_super.call_args[0] - assert call_args[1] == mock_wrapped # Second arg is the client socket - - def test_eventlet_alpn_works_with_handshake_fix(self, eventlet_worker): - """Test that ALPN detection works after handshake fix for eventlet.""" - from gunicorn.workers import geventlet - - mock_ssl_socket = mock.Mock(spec=ssl.SSLSocket) - mock_ssl_socket.selected_alpn_protocol.return_value = "h2" - mock_listener = mock.MagicMock() - - with mock.patch.object(geventlet, 'ssl_wrap_socket', return_value=mock_ssl_socket): - with mock.patch.object(eventlet_worker, 'handle_http2') as mock_h2: - eventlet_worker.handle(mock_listener, mock.MagicMock(), ('127.0.0.1', 8000)) - - # Verify handshake was called (by base_async.handle) - mock_ssl_socket.do_handshake.assert_called_once() - # Verify HTTP/2 handler was invoked - mock_h2.assert_called_once()