Hi all,
This is the backend counterpart to the UI roadmap: what it takes to get Ascender from Django 5.2.17 to 6.1.1.
What Django is
Django is the Python web framework the Ascender backend is built on, supplying the ORM (object-relational mapper) behind every model and migration, the request handling under the REST API, and the settings and management commands the services run from. Ascender was on 5.2.17 when this thread opened and main is on 6.1.1 today, a major that drops deprecated APIs and raises the floors for Python and the database.
Upgrade
Two packages move:
django 5.2.17 -> 6.1.1
djangorestframework 3.17.2 -> 3.18.0
That is the whole list: of the installed distributions that require Django, only DRF has to move.
DRF is mandatory on 6.1 but not on 6.0: rest_framework/views.py imports cc_delim_re from django.utils.cache, which 6.1 removed, so awx/urls.py fails at import. It is not in the release notes. 3.18 lists Framework :: Django :: 6.1.
Blocked: django-guid, with nothing newer published
The marker that applies reads django<6.0,>=4.2 ; python_version >= "3.10" and python_version < "3.13", so on Python 3.12 pip check fails. Its own classifiers claim Framework :: Django :: 6.0 and upstream main carries the same table, so this is a forgotten marker rather than a real incompatibility: they widened the ceiling only on the 3.13 branch.
Resolved by moving the images to Python 3.14, which lands on the >=3.13 branch. 3.14 is in the Rocky 9 AppStream, with setuptools and packaging in crb, so it needs no new repository. Python 3.13 would have needed EPEL for all five packages.
Remove
One line in the requirements file changes:
requirements/requirements.in:18 django>=5.2.17,<6.0 -> django>=6.1.1
The ,<6.0 ceiling was self-imposed, not a dependency's.
Database version floors, and the move to PostgreSQL
Django 6.1 floors PostgreSQL at 15 and SQLite at 3.37. PostgreSQL is fine, though 6.2 may raise it.
SQLite was not fine. The suite ran on SQLite and the devel image is Rocky 9.8 with sqlite-libs-3.34.1, so a Django 6.1 run dies with NotSupportedError: SQLite 3.37 or later is required. pysqlite3-binary looks like the cheap fix and is not: it bundles 3.51.1, but its Connection has no getlimit(), which Django's backend calls for max_query_params, and the suite fails with 2,507 errors.
So I moved the suite to PostgreSQL, which is what Ascender is deployed against anyway. That retires the whole class of problem, since the database becomes a service that's versioned deliberately rather than a library the base image happens to ship. It also surfaced six defects that SQLite had been hiding, three of them in code that ships:
assertNumQueries was inert. awx/main/db/profiled_pg/base.py reimplements force_debug_cursor as a property so it can consult the profile_sql flag, and gave it a setter that discards the value. Django's CaptureQueriesContext enables recording by assigning to exactly that attribute, so every query-count assertion observed zero queries and could only pass by expecting zero.
- The stdout
COPY was never exercised. The sqlite_copy fixture's mock ignored the SQL and returned every event's stdout, so the partition filter WHERE job_created = ..., COPY's row separator, and its {a,b} array rendering were all untested.
inventory_import took a real advisory lock before rejecting its own arguments, because the SQLite path yields without touching the database.
is_testing() failed in xdist workers, silently disabling the post_migrate hook that creates the ServiceID and producing an error that blamed migrations.
- Two
from_db() overrides dropped 6.1's fetch_mode, which would have excluded CredentialType and the vendored dab models from fetch modes entirely.
- The
mock_access fixture issued select_related() with no arguments, meaning "follow every non-null foreign key". A MagicMock attribute is truthy and iterates empty, so the guard passed with no fields.
Tests that assumed SQLite also had to change: primary keys only start at 1 because SQLite resets rowid on rollback, and one test asserted an ordering that is simply undefined when every sort key is identical.
Speed
The suite ended up faster than the SQLite one it replaces:
SQLite, before 347s
PostgreSQL, naive 890s
request-scoped transaction.atomic() 603s
cheap password hasher in tests 379s
after removing the dead sqlite_copy fixtures 320s
Both wins are correctness fixes rather than tuning. conftest calls the view directly, bypassing Django's handler, so production's ATOMIC_REQUESTS never applied and one failed request left the whole test's transaction aborted; wrapping the call removed the need for 61 tests to opt out of transactional isolation. And one PBKDF2 hash costs about 0.7s at 6.1's 1,500,000 iterations, with a user created for nearly every functional test.
Measured and rejected: --dist=load is 19% slower than loadfile because per-test distribution loses fixture reuse, and doubling the worker count is within noise.
Verified
- Python 3.14 + SQLite + Django 5.2.17: 3946 passed, 10 skipped.
- Python 3.14 + PostgreSQL + Django 5.2.17: 3950 passed, 6 skipped, four PostgreSQL-only tests now run.
- Python 3.14 + PostgreSQL + Django 6.1.1: 3950 passed, 6 skipped,
Pull requests
Independent of each other except where noted, and safe to merge in this order:
One thing worth weighing on #814: Django 6.1 leaves extended support in December 2027, four months earlier than 5.2 LTS in April 2028, so this does not buy a longer support horizon. The case for taking it now is that it absorbs the 6.0 and 6.1 breaking changes while nothing is urgent, which makes the hop to 6.2 LTS in April 2027 a small one.