gunicorn icon indicating copy to clipboard operation
gunicorn copied to clipboard

Gevent + Django SynchronousOnlyOperation

Open zN3utr4l opened this issue 1 year ago • 3 comments

gevent==22.10.2
gunicorn==20.1.0
django==3.2.19

"""Gunicorn config file"""

# import os
from core.utilities.env import get_env_value
from core.config.logging import DJANGO_DIR_LOG

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = 'core.wsgi:application'  # 'core.asgi:application' need FastAPI
# The granularity of Error log outputs
loglevel = 'debug'
# The number of worker processes for handling requests (at the moment we don't use more than 2 workers, because the analytical process (celery container) needs so many resources, to be tested)
workers = 2  # os.cpu_count() * 2 + 1
# Use Uvicorn worker for async mode
# worker_class = 'uvicorn.workers.UvicornWorker' need FastAPI
worker_class = 'gevent'
preload_app = True
# The maximum number of requests a worker can handle before being restarted
max_requests = 1000
max_requests_jitter = 50
# The number of seconds to wait for requests to complete
timeout = 300
# The socket to bind
bind = '0.0.0.0:7000'
# accesslog = DJANGO_DIR_LOG + '/gunicorn/access.log'
errorlog = DJANGO_DIR_LOG + '/gunicorn/error.log'
# Redirect stdout/stderr to log file
capture_output = False
# PID file so you can easily fetch process ID
pidfile = '/run/gunicorn/gunicorn.pid'
# Daemonize the Gunicorn process (detach & enter background)
daemon = False
# Set user and group for running Gunicorn
user = 'mambauser'
chdir = '/code/Analyser'

I use django with gunicorn, as long as I use the default worker_class everything is ok, fine, even if the suggested workers (os.cpu_count() * 2 + 1) consume too much ram. I wanted to use Gevent to have I/O optimizations, but when I use it it gives me the error:

File "/opt/conda/lib/python3.9/site-
packages/django/db/models/sql/compiler.py", line 1414, in execute_sql
    with self.connection.cursor() as cursor:
  File "/opt/conda/lib/python3.9/site-packages/django/utils/asyncio.py", line 31, in inner
    raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

I have this problem with sync & async views, in my wsgi.py i put

from pycogreen.gevent import patch_psycopg
from gevent.monkey import patch_all
patch_all()
patch_psycopg()

But nothig change This is my async view:


class ListTests(APIView):
    @async_to_sync
    async def get(self, request, *args, **kwargs):
            test_task = asyncio.create_task(test_repository.count_expired_async())
            ................
            test_count = await test_task
            ............

TraceBack

2023-05-25 13:38:42.680 | ERROR    | .views:get:87 - unhandled exception in get tests information
Traceback (most recent call last):
  File "/opt/conda/lib/python3.9/site-packages/asgiref/sync.py", line 306, in main_wrap
    result = await self.awaitable(*args, **kwargs)
                   │    │          │       └ {}
                   │    │          └ (<views.LisTests object at 0x7f6508874df0>, <rest_framework.request.Request: GET '...
                   │    └ <function ListBadges.get at 0x7f6508cbb670>
                   └ <asgiref.sync.AsyncToSync object at 0x7f6508c371c0>
> File "/views.py", line 64, in get
    test_count = await test_task
                                            └ <coroutine object asyncio.create_task at 0x7f6508848d40>
  File "/views.py", line 47, in asyncio.create_task
    return await task
                 └ <Task finished name='Task-5' coro=<SyncToAsync.__call__() done, defined at /opt/conda/lib/python3.9/site-packages/asgiref/syn...
  File "/opt/conda/lib/python3.9/asyncio/futures.py", line 284, in __await__
    yield self  # This tells Task to wait for completion.
          └ <Task finished name='Task-5' coro=<SyncToAsync.__call__() done, defined at /opt/conda/lib/python3.9/site-packages/asgiref/syn...
  File "/opt/conda/lib/python3.9/asyncio/tasks.py", line 328, in __wakeup
    future.result()
    │      └ <function Future.result at 0x7f652f3ee3a0>
    └ <Task finished name='Task-5' coro=<SyncToAsync.__call__() done, defined at /opt/conda/lib/python3.9/site-packages/asgiref/syn...
  File "/opt/conda/lib/python3.9/asyncio/futures.py", line 201, in result
    raise self._exception
          │    └ SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_async.')
          └ <Task finished name='Task-5' coro=<SyncToAsync.__call__() done, defined at /opt/conda/lib/python3.9/site-packages/asgiref/syn...
  File "/opt/conda/lib/python3.9/asyncio/tasks.py", line 258, in __step
    result = coro.throw(exc)
             │    └ <method 'throw' of 'coroutine' objects>
             └ <coroutine object SyncToAsync.__call__ at 0x7f6508848cc0>
  File "/opt/conda/lib/python3.9/site-packages/asgiref/sync.py", line 448, in __call__
    ret = await asyncio.wait_for(future, timeout=None)
                │       │        └ <Future finished exception=SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_asy...
                │       └ <function wait_for at 0x7f652f38aa60>
                └ <module 'asyncio' from '/opt/conda/lib/python3.9/asyncio/__init__.py'>
  File "/opt/conda/lib/python3.9/asyncio/tasks.py", line 442, in wait_for
    return await fut
                 └ <Future finished exception=SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_asy...
  File "/opt/conda/lib/python3.9/asyncio/futures.py", line 284, in __await__
    yield self  # This tells Task to wait for completion.
          └ <Future finished exception=SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_asy...
  File "/opt/conda/lib/python3.9/asyncio/tasks.py", line 328, in __wakeup
    future.result()
    │      └ <function Future.result at 0x7f652f3ee3a0>
    └ <Future finished exception=SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_asy...
  File "/opt/conda/lib/python3.9/asyncio/futures.py", line 201, in result
    raise self._exception
          │    └ SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_async.')
          └ <Future finished exception=SynchronousOnlyOperation('You cannot call this from an async context - use a thread or sync_to_asy...
  File "/opt/conda/lib/python3.9/site-packages/asgiref/current_thread_executor.py", line 22, in run
    result = self.fn(*self.args, **self.kwargs)
             │        │            └ None
             │        └ None
             └ None
  File "/opt/conda/lib/python3.9/site-packages/asgiref/sync.py", line 490, in thread_handler
    return func(*args, **kwargs)
           │     │       └ {}
           │     └ (functools.partial(<function UsageBasedMaintenanceRepository.count_expired at 0x7f652be1f310>, <main.services.repository.usag...
           └ <built-in method run of Context object at 0x7f650888b780>
  File "/repository/test_repository.py", line 59, in count_expired
    return len(self.get_can_result())
               │    └ <function TestRepository.get_can_result at 0x7f652be1f280>
               └ .repository.test_repository.TestRepository object at 0x7f652be498e0>
  File "/repository/test_repository.py", line 50, in get_can_result
    for test in self.get_started(test_n):
                                   │    │           └ None
                                   │    └ <function TestRepository.get_started at 0x7f652be1f160>
                                   └ <repository.test_repository.TestRepositoryobject at 0x7f652be498e0>
  File "/opt/conda/lib/python3.9/site-packages/django/db/models/query.py", line 280, in __iter__
    self._fetch_all()
    │    └ <function QuerySet._fetch_all at 0x7f652ea4c3a0>
    └ <unprintable QuerySet object>
  File "/opt/conda/lib/python3.9/site-packages/django/db/models/query.py", line 1324, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
    │    │                    │    │               └ <unprintable QuerySet object>
    │    │                    │    └ <class 'django.db.models.query.ModelIterable'>
    │    │                    └ <unprintable QuerySet object>
    │    └ None
    └ <unprintable QuerySet object>
  File "/opt/conda/lib/python3.9/site-packages/django/db/models/query.py", line 51, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
              │        │                         │    │                         │    └ 100
              │        │                         │    │                         └ <django.db.models.query.ModelIterable object at 0x7f6508870880>
              │        │                         │    └ False
              │        │                         └ <django.db.models.query.ModelIterable object at 0x7f6508870880>
              │        └ <function SQLCompiler.execute_sql at 0x7f652756e3a0>
              └ <django.db.models.sql.compiler.SQLCompiler object at 0x7f65088701f0>
  File "/opt/conda/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1173, in execute_sql
    cursor = self.connection.cursor()
             │    │          └ <function BaseDatabaseWrapper.cursor at 0x7f652d4c94c0>
             │    └ <timescale.db.backends.postgresql.base.DatabaseWrapper object at 0x7f65088dc700>
             └ <django.db.models.sql.compiler.SQLCompiler object at 0x7f65088701f0>
  File "/opt/conda/lib/python3.9/site-packages/django/utils/asyncio.py", line 31, in inner
    raise SynchronousOnlyOperation(message)
          │                        └ 'You cannot call this from an async context - use a thread or sync_to_async.'
          └ <class 'django.core.exceptions.SynchronousOnlyOperation'>
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

zN3utr4l avatar May 25 '23 13:05 zN3utr4l

You are trying to use asyncio with a gevent worker. For now I would suggest to use the uvicorn gunicorn worker to handle it. Hope it helps.

benoitc avatar Jun 23 '23 07:06 benoitc

My views are some async (but with the @async_to_sync wrap which should turn them into sync) and some sync. Why should I use uvicorn, I already tried and from what I understand uvicorn requires all views to be async, better implement FastAPI, and not django with Uvicorn.

zN3utr4l avatar Jun 23 '23 07:06 zN3utr4l

@zN3utr4l gevent has the same purpose of asyncio, make Python code cooperative when doing network IO. But gevent covers much more APIs which can work cooperatively like file reading, thread sleep, etc and can do that transparently through monkey patching. You should not be mixing asyncio with gevent.

Django asyncio support is still primitive, most of the async features are backed by a thread pool used through the sync_to_async decorator. To make sync views compatible, Django still creates a new thread for every request. You don't get any benefit other than integrating legacy sync code with more modern, asyncio code. Performance wise it's likely worse than a regular, full sync Django application.

You are better using gevent only + psycogreen for postgresql database.

uvicorn worker is not aware of Django. It is an ASGI application server, it should work with any web application which conforms with the ASGI standard. Django's asgi.py file defines the entry point of its ASGI application, uvicorn will load that. Internally django will use the async_to_sync decorator to make async code compatible with sync code, and use sync_to_async for the opposite.

HMaker avatar Sep 11 '23 19:09 HMaker