Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uvicorn worker under gunicorn can't handle more than 998 open connections. #22

Open
iamdbychkov opened this issue Nov 11, 2024 · 0 comments

Comments

@iamdbychkov
Copy link

iamdbychkov commented Nov 11, 2024

First of all, if it's not how you fill up the issue in this project - I'm sorry, will gladly fix.

I've experienced some issues with uvicorn.workers.UvicornWorker.

Consider this simple example:

requirements.txt

aiohttp==3.10.10
gunicorn==23.0.0
uvicorn==0.32.0
uvloop==0.21.0

server.py

async def app(scope, receive, send):
    print(f'Request #{scope["query_string"]}')
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

client.py

import asyncio
import signal
import time

import aiohttp

RUNNING = True
REQUESTS=999
CNT = 0

def signal_handler(*args, **kwargs):
    global RUNNING
    RUNNING = False

async def send_request(session):
    global CNT
    CNT += 1
    await session.post(f'http://127.0.0.1:8000/test?{CNT}', json={})


async def main():
    session = aiohttp.ClientSession(
        connector=aiohttp.TCPConnector(limit=5000, ttl_dns_cache=60)
    )
    last = 0
    tasks = set()
    while RUNNING:
        last = time.time()
        for _ in range(REQUESTS):
            task = asyncio.create_task(send_request(session))
            tasks.add(task)
            task.add_done_callback(tasks.discard)

        c_time = time.time()
        sleep_time = 1.0 - (c_time - last)
        if sleep_time < 0:
            sleep_time = 0
        print(f'Sleeping for {sleep_time:.2f} seconds')
        await asyncio.sleep(sleep_time)
    
    print('', 'closing aiohttp session')
    await session.close()


if __name__ == '__main__':
    signal.signal(signal.SIGINT, signal_handler)
    asyncio.run(main())

running a server:

gunicorn server:app -k uvicorn.workers.UvicornWorker --error-logfile - --access-logfile -

runnin a client:

python client.py

will result in a error while processing last request:

future: <Task finished name='Task-1000' coro=<send_request() done, defined at /tmp/test/venv/client.py:18> exception=ClientOSError(32, 'Broken pipe')>
Traceback (most recent call last):
  File "/usr/lib64/python3.12/asyncio/selector_events.py", line 1075, in write
    n = self._sock.send(data)
        ^^^^^^^^^^^^^^^^^^^^^
BrokenPipeError: [Errno 32] Broken pipe

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tmp/test/client.py", line 21, in send_request
    await session.post(f'http://127.0.0.1:8000/test?{CNT}', json={})
  File "/tmp/test/venv/lib64/python3.12/site-packages/aiohttp/client.py", line 690, in _request
    await resp.start(conn)
  File "/tmp/test/venv/lib64/python3.12/site-packages/aiohttp/client_reqrep.py", line 1058, in start
    message, payload = await protocol.read()  # type: ignore[union-attr]
                       ^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/test/venv/lib64/python3.12/site-packages/aiohttp/streams.py", line 643, in read
    await self._waiter
aiohttp.client_exceptions.ClientOSError: [Errno 32] Broken pipe
^C closing aiohttp session

Once you've changed REQUESTS constant to 998 everything seems to work normally.

If you change worker to H11 Worker everything works okay even on higher number of requests.
If you run bare uvicorn everything works okay.

Currently I'm unable to investigate the issue further. The only difference i've found is the loop implementation, UvicornWorker uses uvloop, but creating a server to serve requests with uvloop without uvicorn + gunicorn seems to work ok. Maybe it has something to do with the sockets which gunicorn passes to worker and which are used to create server with uvloop?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant