[Triage] gthread evolution: lifecycle of an accepted connection #3634
aszubarev
started this conversation in
Issue Triage
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Disclaimer
This discussion focuses on a suggestion to improve the lifecycle of an accepted connection. But to understand it you first need to consider the evolution of the
gthreadworker. An improvement suggestion will be described at the endThe evolution of gthread
v20.1.0: gthread deadlock
The diagram below shows the architecture of the
gthreadworker versionv20.1.0. It can be divided into three parts: the event loop, request processing, and graceful shutdown.At the beginning, the listen-socket is switched to non-blocking mode. Then the callback
acceptis registered with thepoller. After that, an infinite loop is started, which waits for events from thepollerwith a timeout of 1 sec. If no events are received during this interval, the loop performs any final operations and then waits again.When a connection request is received, the callback
acceptis triggered. Inside it, the client socket is made blocking. The request processing task is submitted in the thread pool, and the callbackfinish_requestis registered on theFutureinstance.When a pool thread starts handling the request, it reads data from the client in blocking mode. After receiving all the data from the client, the request counter (
nr_conns) is compared with the--max-requestsvalue. If the counter exceeds this threshold, the worker’salivestatus is set toFalse. This causes the main thread to exit the event loop, after which the graceful shutdown procedure is started. It is important to note that the processing of the request itself continues in a dedicated pool thread.When processing is completed, the
finish_requestcallback is called in the same thread. It determines whether to keep the connection or close it immediately. If the connection is kept, the client socket is switched to non-blocking mode, a timeout is set, and another callback (reuse_connection) is registered with thepoller. The only difference betweenreuse_connectionandacceptis thatreuse_connectiondoes not establish a new connection; otherwise, their behavior is identical.The main conclusion is that reading data from the client blocks the thread. As soon as the server accepts the connection, the task is immediately added to the pool. If the client establishes a connection but does not transmit data, the thread will be blocked waiting for packets. This is what caused the issue «Gunicorn gthread deadlock #2917». This behavior was expected, and the official deployment guide even recommends running the application server behind a reverse proxy. Later, the pull request «gthread: only read sockets when they are readable #2918» was accepted and included in the
v21.0.0release.v21.0.0: resilience to speculative connections
The main goal of the changes is protection against speculative connections. It was decided to submit the request processing task into the thread pool only after the client actually starts sending data. The architecture of version
v21.0.0is as follows:Yes, this solved the problem of speculative connections, but along with the new architecture came new bugs. Users widely encountered the issue “Connection reset during max-requests auto-restart with gthread #3038”: the client receive 502 errors.
This issue occurs in the following cases:
--max-requestsThe cause of this issue is a specificity of the graceful shutdown.
After exiting the loop, the following steps are executed:
pollerlistensocketsClosing the
polleralso closes all sockets registered in it. After a successfulaccept, the processing task was not submitted into the pool. Instead, the client socket was registered with thepollerto wait for data. If at that moment the worker changed itsalivestatus toFalse, then the just-accepted client socket was immediately closed. A client sending a request or reading a response would then receive a «Connection reset by peer» error.A similar situation occurs with Keep-Alive connections. The server sets a timeout on them and registers them in the
pollerto wait for a new request. A client sending a request over such a connection assumes it is still alive, but the server may have closed it earlier, even if the timeouts are correctly configured.v24.0.0: refactoring «Graceful Shutdown»
This release includes the following changes:
acceptlogic: the request processing task is again submitted into the pool immediately after the connection is establishedPollableMethodQueue(see #3440)The return to the previous
acceptlogic fixes issue #3038. The new implementation of graceful shutdown ensures that keep-alive connections are properly terminated.After exiting the main loop, the listen sockets are unregistered from the poller. This ensures that no new connections will be accepted in the next «shutdown» cycle. However, as long as the server still has active connections, it waits for new requests to arrive on them.
In other words,
gthreadnow follows the rule: if a timeout is set on a connection, the worker waits for a new request until that timeout expires. When the client and server timeouts are aligned, the close race disappears.A side effect of this implementation is that the
--keep-aliveparameter directly affects the server's shutdown time: the longer this timeout, the longer graceful shutdown can take.v25.2.0: prevent thread pool exhaustion
Release v24.0.0 was again affected by issue «Gunicorn gthread deadlock #2917». As a result, an interesting pull request «fix(gthread): prevent thread pool exhaustion from slow clients #3519» was accepted and included in release
v25.2.0.Worker threads now wait up to 5 seconds for data. If no data arrives, the connection is deferred back to the main poller, freeing the thread for other work.
Improvement suggestion
Instead of waiting for client data in a pool thread, we can wait for it in the main thread — an approach similar to what was used in versions
v21.0.0tov23.0.0. But this time, set a timeout for the connection. When the connection expires, it will be closed in themurder_pendingmethod.Advantages:
pollerfor prevent thread pool exhaustionThe graceful shutdown refactoring in v24+ prevents issue #3038 from occurring. This makes it possible to submit the request processing task only when data is available.
Full implementation example #3635
All reactions