mirror of
https://github.com/django/django.git
synced 2025-10-31 09:41:08 +00:00
Fixed #30451 -- Added ASGI handler and coroutine-safety.
This adds an ASGI handler, asgi.py file for the default project layout, a few async utilities and adds async-safety to many parts of Django.
This commit is contained in:
committed by
Mariusz Felisiak
parent
cce47ff65a
commit
a415ce70be
@@ -17,6 +17,7 @@ from django.db.backends.signals import connection_created
|
||||
from django.db.transaction import TransactionManagementError
|
||||
from django.db.utils import DatabaseError, DatabaseErrorWrapper
|
||||
from django.utils import timezone
|
||||
from django.utils.asyncio import async_unsafe
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
NO_DB_ALIAS = '__no_db__'
|
||||
@@ -177,6 +178,7 @@ class BaseDatabaseWrapper:
|
||||
|
||||
# ##### Backend-specific methods for creating connections #####
|
||||
|
||||
@async_unsafe
|
||||
def connect(self):
|
||||
"""Connect to the database. Assume that the connection is closed."""
|
||||
# Check for invalid configurations.
|
||||
@@ -210,6 +212,7 @@ class BaseDatabaseWrapper:
|
||||
"Connection '%s' cannot set TIME_ZONE because its engine "
|
||||
"handles time zones conversions natively." % self.alias)
|
||||
|
||||
@async_unsafe
|
||||
def ensure_connection(self):
|
||||
"""Guarantee that a connection to the database is established."""
|
||||
if self.connection is None:
|
||||
@@ -251,10 +254,12 @@ class BaseDatabaseWrapper:
|
||||
|
||||
# ##### Generic wrappers for PEP-249 connection methods #####
|
||||
|
||||
@async_unsafe
|
||||
def cursor(self):
|
||||
"""Create a cursor, opening a connection if necessary."""
|
||||
return self._cursor()
|
||||
|
||||
@async_unsafe
|
||||
def commit(self):
|
||||
"""Commit a transaction and reset the dirty flag."""
|
||||
self.validate_thread_sharing()
|
||||
@@ -264,6 +269,7 @@ class BaseDatabaseWrapper:
|
||||
self.errors_occurred = False
|
||||
self.run_commit_hooks_on_set_autocommit_on = True
|
||||
|
||||
@async_unsafe
|
||||
def rollback(self):
|
||||
"""Roll back a transaction and reset the dirty flag."""
|
||||
self.validate_thread_sharing()
|
||||
@@ -274,6 +280,7 @@ class BaseDatabaseWrapper:
|
||||
self.needs_rollback = False
|
||||
self.run_on_commit = []
|
||||
|
||||
@async_unsafe
|
||||
def close(self):
|
||||
"""Close the connection to the database."""
|
||||
self.validate_thread_sharing()
|
||||
@@ -313,6 +320,7 @@ class BaseDatabaseWrapper:
|
||||
|
||||
# ##### Generic savepoint management methods #####
|
||||
|
||||
@async_unsafe
|
||||
def savepoint(self):
|
||||
"""
|
||||
Create a savepoint inside the current transaction. Return an
|
||||
@@ -333,6 +341,7 @@ class BaseDatabaseWrapper:
|
||||
|
||||
return sid
|
||||
|
||||
@async_unsafe
|
||||
def savepoint_rollback(self, sid):
|
||||
"""
|
||||
Roll back to a savepoint. Do nothing if savepoints are not supported.
|
||||
@@ -348,6 +357,7 @@ class BaseDatabaseWrapper:
|
||||
(sids, func) for (sids, func) in self.run_on_commit if sid not in sids
|
||||
]
|
||||
|
||||
@async_unsafe
|
||||
def savepoint_commit(self, sid):
|
||||
"""
|
||||
Release a savepoint. Do nothing if savepoints are not supported.
|
||||
@@ -358,6 +368,7 @@ class BaseDatabaseWrapper:
|
||||
self.validate_thread_sharing()
|
||||
self._savepoint_commit(sid)
|
||||
|
||||
@async_unsafe
|
||||
def clean_savepoints(self):
|
||||
"""
|
||||
Reset the counter used to generate unique savepoint ids in this thread.
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import utils
|
||||
from django.db.backends import utils as backend_utils
|
||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||
from django.utils.asyncio import async_unsafe
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
try:
|
||||
@@ -223,6 +224,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
kwargs.update(options)
|
||||
return kwargs
|
||||
|
||||
@async_unsafe
|
||||
def get_new_connection(self, conn_params):
|
||||
return Database.connect(**conn_params)
|
||||
|
||||
@@ -242,6 +244,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute('; '.join(assignments))
|
||||
|
||||
@async_unsafe
|
||||
def create_cursor(self, name=None):
|
||||
cursor = self.connection.cursor()
|
||||
return CursorWrapper(cursor)
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import utils
|
||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||
from django.utils.asyncio import async_unsafe
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
@@ -221,6 +222,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
del conn_params['use_returning_into']
|
||||
return conn_params
|
||||
|
||||
@async_unsafe
|
||||
def get_new_connection(self, conn_params):
|
||||
return Database.connect(
|
||||
user=self.settings_dict['USER'],
|
||||
@@ -269,6 +271,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
if not self.get_autocommit():
|
||||
self.commit()
|
||||
|
||||
@async_unsafe
|
||||
def create_cursor(self, name=None):
|
||||
return FormatStylePlaceholderCursor(self.connection)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ PostgreSQL database backend for Django.
|
||||
Requires psycopg 2: http://initd.org/projects/psycopg2
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
@@ -15,6 +16,7 @@ from django.db.backends.utils import (
|
||||
CursorDebugWrapper as BaseCursorDebugWrapper,
|
||||
)
|
||||
from django.db.utils import DatabaseError as WrappedDatabaseError
|
||||
from django.utils.asyncio import async_unsafe
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import SafeString
|
||||
from django.utils.version import get_version_tuple
|
||||
@@ -177,6 +179,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
conn_params['port'] = settings_dict['PORT']
|
||||
return conn_params
|
||||
|
||||
@async_unsafe
|
||||
def get_new_connection(self, conn_params):
|
||||
connection = Database.connect(**conn_params)
|
||||
|
||||
@@ -217,6 +220,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
if not self.get_autocommit():
|
||||
self.connection.commit()
|
||||
|
||||
@async_unsafe
|
||||
def create_cursor(self, name=None):
|
||||
if name:
|
||||
# In autocommit mode, the cursor will be used outside of a
|
||||
@@ -227,12 +231,34 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
|
||||
return cursor
|
||||
|
||||
@async_unsafe
|
||||
def chunked_cursor(self):
|
||||
self._named_cursor_idx += 1
|
||||
# Get the current async task
|
||||
# Note that right now this is behind @async_unsafe, so this is
|
||||
# unreachable, but in future we'll start loosening this restriction.
|
||||
# For now, it's here so that every use of "threading" is
|
||||
# also async-compatible.
|
||||
try:
|
||||
if hasattr(asyncio, 'current_task'):
|
||||
# Python 3.7 and up
|
||||
current_task = asyncio.current_task()
|
||||
else:
|
||||
# Python 3.6
|
||||
current_task = asyncio.Task.current_task()
|
||||
except RuntimeError:
|
||||
current_task = None
|
||||
# Current task can be none even if the current_task call didn't error
|
||||
if current_task:
|
||||
task_ident = str(id(current_task))
|
||||
else:
|
||||
task_ident = 'sync'
|
||||
# Use that and the thread ident to get a unique name
|
||||
return self._cursor(
|
||||
name='_django_curs_%d_%d' % (
|
||||
# Avoid reusing name in other threads
|
||||
name='_django_curs_%d_%s_%d' % (
|
||||
# Avoid reusing name in other threads / tasks
|
||||
threading.current_thread().ident,
|
||||
task_ident,
|
||||
self._named_cursor_idx,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ from django.db import utils
|
||||
from django.db.backends import utils as backend_utils
|
||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||
from django.utils import timezone
|
||||
from django.utils.asyncio import async_unsafe
|
||||
from django.utils.dateparse import parse_datetime, parse_time
|
||||
from django.utils.duration import duration_microseconds
|
||||
|
||||
@@ -191,6 +192,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
kwargs.update({'check_same_thread': False, 'uri': True})
|
||||
return kwargs
|
||||
|
||||
@async_unsafe
|
||||
def get_new_connection(self, conn_params):
|
||||
conn = Database.connect(**conn_params)
|
||||
conn.create_function("django_date_extract", 2, _sqlite_datetime_extract)
|
||||
@@ -248,6 +250,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
def create_cursor(self, name=None):
|
||||
return self.connection.cursor(factory=SQLiteCursorWrapper)
|
||||
|
||||
@async_unsafe
|
||||
def close(self):
|
||||
self.validate_thread_sharing()
|
||||
# If database is in memory, closing the connection destroys the
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import pkgutil
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from threading import local
|
||||
|
||||
from asgiref.local import Local
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
@@ -139,7 +140,12 @@ class ConnectionHandler:
|
||||
like settings.DATABASES).
|
||||
"""
|
||||
self._databases = databases
|
||||
self._connections = local()
|
||||
# Connections needs to still be an actual thread local, as it's truly
|
||||
# thread-critical. Database backends should use @async_unsafe to protect
|
||||
# their code from async contexts, but this will give those contexts
|
||||
# separate connections in case it's needed as well. There's no cleanup
|
||||
# after async contexts, though, so we don't allow that if we can help it.
|
||||
self._connections = Local(thread_critical=True)
|
||||
|
||||
@cached_property
|
||||
def databases(self):
|
||||
|
||||
Reference in New Issue
Block a user