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.
This commit is contained in:
Benoit Chesneau 2026-05-05 00:36:46 +02:00
parent 68843c8893
commit 201df19a80
17 changed files with 16 additions and 336 deletions

View File

@ -95,7 +95,6 @@ body:
- sync (default)
- gthread
- gevent
- eventlet
- tornado
- asgi (beta)
- custom

View File

@ -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

View File

@ -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+

View File

@ -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"

View File

@ -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 |

View File

@ -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.

View File

@ -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)

View File

@ -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`

View File

@ -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.
"""

View File

@ -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):

View File

@ -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",

View File

@ -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()

View File

@ -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."
)

View File

@ -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."
)

View File

@ -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

View File

@ -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

View File

@ -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()