Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/data/conda/geolibs-pg17.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: geodjango
channels:
- conda-forge
dependencies:
- python=3.14
- pip=26.*
- postgresql=17.*
- postgis=3.5.*
- libgdal=3.11.*
- geos=3.14.*
- proj=9.6.*
11 changes: 11 additions & 0 deletions .github/workflows/data/conda/geolibs-pg18.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: geodjango
channels:
- conda-forge
dependencies:
- python=3.14
- pip=26.*
- postgresql=18.*
- postgis=3.6.*
- libgdal=3.13.*
- geos=3.14.*
- proj=9.8.*
60 changes: 27 additions & 33 deletions .github/workflows/postgis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,48 @@ jobs:
postgis:
if: contains(github.event.pull_request.labels.*.name, 'geodjango')
runs-on: ubuntu-latest
defaults:
run:
# Use Conda shell in all steps. See https://github.com/marketplace/actions/setup-miniconda#use-a-default-shell
shell: bash -el {0}
strategy:
fail-fast: false
matrix:
postgis-version: ["latest", "18-3.6-alpine", "17-master"]
name: PostGIS ${{ matrix.postgis-version }}
services:
postgres:
image: postgis/postgis:${{ matrix.postgis-version }}
env:
POSTGRES_DB: geodjango
POSTGRES_USER: user
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
timeout-minutes: 60
include:
- conda-environment-file: ".github/workflows/data/conda/geolibs-pg17.yml"
pg_major: "17"
- conda-environment-file: ".github/workflows/data/conda/geolibs-pg18.yml"
pg_major: "18"
name: PostgreSQL ${{ matrix.pg_major }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
- name: Setup Miniforge
# Pinned to v4.0.1
uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5
with:
python-version: '3.14'
cache: 'pip'
cache-dependency-path: 'tests/requirements/py3.txt'
- name: Update apt repo
run: sudo apt update
activate-environment: geodjango
environment-file: ${{ matrix.conda-environment-file }}
channel-priority: strict
- name: Install libmemcached-dev for pylibmc
run: sudo apt install -y libmemcached-dev
- name: Install geospatial dependencies
run: sudo apt install -y binutils libproj-dev gdal-bin
- name: Print PostGIS versions
env:
POSTGRES_DB: geodjango
POSTGRES_USER: user
POSTGRES_PASSWORD: postgres
run: |
PGPASSWORD=$POSTGRES_PASSWORD psql -U $POSTGRES_USER -d $POSTGRES_DB -h localhost -c "SELECT PostGIS_full_version();"
- name: Install and upgrade packaging tools
run: python -m pip install --upgrade pip wheel
- run: python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
- name: Create PostgreSQL settings file
run: mv ./.github/workflows/data/test_postgis.py.tpl ./tests/test_postgis.py
- name: Initialize and start local PostgreSQL
env:
PGPASSWORD: postgres
run: |
initdb -D "$GITHUB_WORKSPACE/.tmp/pgdata" --username="user" --auth=scram-sha-256 --pwfile=<(echo "$PGPASSWORD")
pg_ctl -D "$GITHUB_WORKSPACE/.tmp/pgdata" -w start
psql -U user -d postgres -c "CREATE EXTENSION IF NOT EXISTS postgis;"
psql -U user -d postgres -c "SELECT PostGIS_full_version();"
- name: Print geospatial versions
run: |
python -c "from django.contrib.gis import gdal, geos; print(f'GDAL: {gdal.gdal_version()}'); print(f'GEOS: {geos.geos_version()}')"
- name: Run PostGIS tests
run: python -Wall tests/runtests.py -v2 --settings=test_postgis
20 changes: 7 additions & 13 deletions django/middleware/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@
learn_cache_key,
patch_response_headers,
patch_vary_headers,
split_header_value,
)
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import parse_http_date_safe
from django.utils.http import parse_http_date_safe, split_directive_names


class UpdateCacheMiddleware(MiddlewareMixin):
Expand Down Expand Up @@ -105,17 +104,12 @@ def process_response(self, request, response):
return response

# Don't cache responses when the Cache-Control header is set to
# private, no-cache, or no-store.
cache_control = response.get("Cache-Control", "").lower()
cache_control_parts = list(split_header_value(cache_control))
if cache_control and any(
directive in cache_control_parts
for directive in (
"private",
"no-cache",
"no-store",
)
):
# private, no-cache, or no-store. Qualified forms like
# `private="Set-Cookie"` reduce to their directive name and match too.
cache_control_parts = set(
split_directive_names(response.get("Cache-Control", ""))
)
if cache_control_parts.intersection({"private", "no-cache", "no-store"}):
return response

# Don't cache responses when the Vary header contains '*'.
Expand Down
6 changes: 3 additions & 3 deletions django/middleware/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.utils.cache import get_conditional_response, set_response_etag
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import parse_http_date_safe, split_header_value
from django.utils.http import parse_http_date_safe, split_directive_names


class ConditionalGetMiddleware(MiddlewareMixin):
Expand Down Expand Up @@ -37,5 +37,5 @@ def process_response(self, request, response):

def needs_etag(self, response):
"""Return True if an ETag header should be added to response."""
cache_control_headers = split_header_value(response.get("Cache-Control", ""))
return all(header.lower() != "no-store" for header in cache_control_headers)
directives = split_directive_names(response.get("Cache-Control", ""))
return "no-store" not in directives
2 changes: 1 addition & 1 deletion django/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def patch_cache_control(response, **kwargs):
def dictitem(s):
t = s.split("=", 1)
if len(t) > 1:
return (t[0].lower(), t[1])
return (t[0].strip().lower(), t[1])
else:
return (t[0].lower(), True)

Expand Down
14 changes: 14 additions & 0 deletions django/utils/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ def split_header_value(value, sep=","):
yield stripped


def split_directive_names(value):
"""Yield the lowercased directive names from an HTTP header value.

Any qualified value is discarded, so that qualified forms permitted by
RFC 9111 (e.g. `private="Set-Cookie"`) reduce to their directive name
("private").

Use to check for the presence of a directive when its value is not needed;
use `split_header_value()` when the value matters (e.g. `max-age`).
"""
for part in split_header_value(value):
yield part.split("=", 1)[0].strip().lower()


def parse_etags(etag_str):
"""
Parse a string of ETags given in an If-None-Match or If-Match header as
Expand Down
9 changes: 1 addition & 8 deletions docs/faq/models.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,7 @@ immediately after :djadmin:`migrate` was executed.
Do Django models support multiple-column primary keys?
======================================================

No. Only single-column primary keys are supported.

But this isn't an issue in practice, because there's nothing stopping you from
adding other constraints (using the ``unique_together`` model option or
creating the constraint directly in your database), and enforcing the
uniqueness at that level. Single-column primary keys are needed for things such
as the admin interface to work; e.g., you need a single value to specify
an object to edit or delete.
Partially. See :doc:`/topics/composite-primary-key`.

Does Django support NoSQL databases?
====================================
Expand Down
106 changes: 61 additions & 45 deletions docs/topics/async.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Asynchronous support
.. currentmodule:: asgiref.sync

Django has support for writing asynchronous ("async") views, along with an
entirely async-enabled request stack if you are running under
:doc:`ASGI </howto/deployment/asgi/index>`. Async views will still work under
WSGI, but with performance penalties, and without the ability to have efficient
long-running requests.
entirely async-enabled request stack if you are running under :doc:`ASGI
</howto/deployment/asgi/index>`. Async views will still work under WSGI, but
with a small per-request adaptation cost (see :ref:`async_performance`), and
without the ability to have efficient long-running requests.

We're still working on async support for the ORM and other parts of Django.
You can expect to see this in future releases. For now, you can use the
:func:`sync_to_async` adapter to interact with the sync parts of Django.
There is also a whole range of async-native Python libraries that you can
integrate with.
Many parts of Django provide asynchronous APIs, including :ref:`the ORM
<async-queries>`, the cache framework, authentication, sessions, and signals.
For other code, the :func:`sync_to_async` adapter is a low-cost bridge (see
:ref:`async_performance`). A wide range of async-native Python libraries can
also be integrated.

Async views
===========
Expand Down Expand Up @@ -44,18 +44,18 @@ other exciting response types.
If you want to use these, you will need to deploy Django using
:doc:`ASGI </howto/deployment/asgi/index>` instead.

.. warning::
.. note::

You will only get the benefits of a fully-asynchronous request stack if you
have *no synchronous middleware* loaded into your site. If there is a piece
of synchronous middleware, then Django must use a thread per request to
safely emulate a synchronous environment for it.
A fully asynchronous request stack requires async middleware end-to-end.
Where a piece of synchronous middleware sits between an ASGI server and an
async view, Django adapts it by running it in its own thread; see
:ref:`async_performance` for the cost trade-off.

Middleware can be built to support :ref:`both sync and async
<async-middleware>` contexts. Some of Django's middleware is built like
this, but not all. To see what middleware Django has to adapt for, you can
turn on debug logging for the ``django.request`` logger and look for log
messages about *"Asynchronous handler adapted for middleware ..."*.
Django's bundled middleware supports both :ref:`sync and async
<async-middleware>`. Third-party middleware may not. To see which
middleware Django adapts, turn on debug logging for the ``django.request``
logger and look for log messages about *"Asynchronous handler adapted for
middleware ..."*.

In both ASGI and WSGI mode, you can still safely use asynchronous support to
run code concurrently rather than serially. This is especially handy when
Expand Down Expand Up @@ -140,7 +140,7 @@ function.
Queries & the ORM
-----------------

With some exceptions, Django can run ORM queries asynchronously as well::
With some exceptions, Django can run ORM queries asynchronously::

async for author in Author.objects.filter(name__startswith="A"):
book = await author.books.afirst()
Expand All @@ -153,7 +153,7 @@ Detailed notes can be found in :ref:`async-queries`, but in short:
* ``async for`` is supported on all QuerySets (including the output of
``values()`` and ``values_list()``.)

Django also supports some asynchronous model methods that use the database::
Asynchronous model methods that use the database are also supported::

async def make_book(*args, **kwargs):
book = Book(...)
Expand All @@ -171,7 +171,9 @@ synchronous function and call it using :func:`sync_to_async`.
:ref:`Persistent database connections <persistent-database-connections>`, set
via the :setting:`CONN_MAX_AGE` setting, should also be disabled in async mode.
Instead, use your database backend's built-in connection pooling if available,
or investigate a third-party connection pooling option if required.
or investigate a third-party connection pooling option if required. As in
synchronous Django, concurrent requests in a single process share that pool, so
size it to the target in-flight query concurrency.

.. _async_performance:

Expand All @@ -180,20 +182,31 @@ Performance

When running in a mode that does not match the view (e.g. an async view under
WSGI, or a traditional sync view under ASGI), Django must emulate the other
call style to allow your code to run. This context-switch causes a small
performance penalty of around a millisecond.

This is also true of middleware. Django will attempt to minimize the number of
context-switches between sync and async. If you have an ASGI server, but all
your middleware and views are synchronous, it will switch just once, before it
enters the middleware stack.
call style to allow your code to run. The per-call cost of this adaptation is
small: tens of microseconds in the in-request ASGI path, where the running
event loop is reused, and a few hundred microseconds in the cold-start path
used by management commands, background tasks, and scripts. Against typical
request times measured in milliseconds, this is rarely visible in itself, but
can become so under GIL contention as the number of active threads grows.

If you find yourself wrapping individual rows or operations in a tight loop,
restructure your code so the loop runs inside a single :func:`sync_to_async`
(or :func:`async_to_sync`) crossing. The per-call cost of the context switch is
then spread across the whole loop and effectively disappears.

The same per-call adaptation cost applies to middleware. Django will attempt to
minimize the number of context-switches between sync and async. If you have an
ASGI server, but all your middleware and views are synchronous, it will switch
just once, before it enters the middleware stack.

However, if you put synchronous middleware between an ASGI server and an
asynchronous view, it will have to switch into sync mode for the middleware and
then back to async mode for the view. Django will also hold the sync thread
open for middleware exception propagation. This may not be noticeable at first,
but adding this penalty of one thread per request can remove any async
performance advantage.
open for middleware exception propagation. For request/response views that hit
the ORM and return, this is not usually a meaningful penalty. It matters most
when you are using ASGI for high in-process concurrency over non-ORM I/O (for
example upstream HTTP fan-out, server-sent events, or other long-lived
requests), where the extra thread per request caps that concurrency.

You should do your own performance testing to see what effect ASGI versus WSGI
has on your code. In some cases, there may be a performance increase even for
Expand Down Expand Up @@ -232,8 +245,8 @@ Async safety
Certain key parts of Django are not able to operate safely in an async
environment, as they have global state that is not coroutine-aware. These parts
of Django are classified as "async-unsafe", and are protected from execution in
an async environment. The ORM is the main example, but there are other parts
that are also protected in this way.
an async environment. The synchronous API of the ORM is the main example, but
there are other parts that are also protected in this way.

If you try to run any of these parts from a thread where there is a *running
event loop*, you will get a
Expand Down Expand Up @@ -334,9 +347,12 @@ Threadlocals and contextvars values are preserved across the boundary in both
directions.

:func:`async_to_sync` is essentially a more powerful version of the
:func:`asyncio.run` function in Python's standard library. As well
as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of
:func:`sync_to_async` when that wrapper is used below it.
:func:`asyncio.run` function in Python's standard library. As well as ensuring
threadlocals work, it also enables the ``thread_sensitive`` mode of
:func:`sync_to_async` when that wrapper is used below it. In the cold path
(no running event loop) it pays the cost of starting a fresh event loop, like
:func:`asyncio.run`; when an event loop is already running (the in-request ASGI
case), the running loop is reused and the cost drops accordingly.

``sync_to_async()``
-------------------
Expand Down Expand Up @@ -369,14 +385,6 @@ thread, so :func:`sync_to_async` has two threading modes:
* ``thread_sensitive=False``: the sync function will run in a brand new thread
which is then closed once the invocation completes.

.. warning::

``asgiref`` version 3.3.0 changed the default value of the
``thread_sensitive`` parameter to ``True``. This is a safer default, and in
many cases interacting with Django the correct value, but be sure to
evaluate uses of ``sync_to_async()`` if updating ``asgiref`` from a prior
version.

Thread-sensitive mode is quite special, and does a lot of work to run all
functions in the same thread. Note, though, that it *relies on usage of*
:func:`async_to_sync` *above it in the stack* to correctly run things on the
Expand All @@ -396,6 +404,14 @@ always be in a *different* thread to any async code that is calling it, so you
should avoid passing raw database handles or other thread-sensitive references
around.

Within a single request, multiple ``thread_sensitive`` calls serialize on that
request's worker thread, but each request gets its own per-context worker, so
concurrent requests do *not* serialize against each other. This mirrors
Django's connection-per-thread model, and the same constraint applies in other
async database libraries, where concurrent queries on a single connection
serialize on a lock. To support more concurrent requests, increase the
connection pool size accordingly rather than disabling ``thread_sensitive``.

In practice this restriction means that you should not pass features of the
database ``connection`` object when calling ``sync_to_async()``. Doing so will
trigger the thread safety checks:
Expand Down
Loading
Loading