Skip to content

Bump django from 4.2.30 to 5.2.16 - #118

Merged
ross merged 8 commits into
mainfrom
dependabot/pip/django-5.2.16
Aug 10, 2026
Merged

ross merged 8 commits into
mainfrom
dependabot/pip/django-5.2.16

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 10, 2026

Copy link
Copy Markdown
Contributor

Bumps django from 4.2.30 to 5.2.16.

Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Bumps [django](https://github.com/django/django) from 4.2.30 to 5.2.16.
- [Commits](django/django@4.2.30...5.2.16)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.16
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file python Pull requests that update python code labels Aug 10, 2026
ross added 7 commits August 10, 2026 10:37
Django 5.1 dropped support for the Meta.index_together option, which broke
app startup on this branch's Django 5.2 bump (TypeError: 'class Meta' got
invalid attribute(s): index_together). Replace it with the modern
Meta.indexes API.

The migration can't just rename the old index in place: the composite
index_together index was already silently lost on SQLite because
0003_make_workspace_nonnull's AlterField rebuilds the whole table, and
SQLite's table-rebuild path only restores indexes tracked via Meta.indexes,
not the legacy index_together. On MySQL (production) the AlterField is an
in-place ALTER TABLE, so the old index is untouched and still physically
present under its old auto-generated name. The new migration accounts for
both cases: it drops the old index by its (deterministic, backend-
independent) name if found, then adds the new named index.
models.Index() validates that fields/expressions are non-empty in its
constructor, even though remove_index()/remove_sql() only actually need
the .name to build the DROP INDEX statement. My local SQLite testing
didn't exercise this: the legacy index doesn't physically exist there (see
the previous commit's message), so drop_legacy_index() short-circuited
before constructing the Index. Production's MySQL DB does still have it,
so it hit the construction and raised ValueError on startup.

Verified by manually recreating the legacy index on a SQLite DB at
migration 0003 and confirming 0004 now drops it and adds the new one.
…ocket

script/run launches gunicorn with --preload, which imports simone.urls (and
therefore starts the Cron background thread, which opens a DB connection on
its first tick) in the master process before workers are forked. fork()
duplicates the master's open MySQL socket fd into the worker, so the
master's Cron thread and the worker end up sharing one underlying
connection. Once both sides use it concurrently they corrupt each other's
protocol state on that socket and hang forever waiting on a response that
will never arrive — no exception, no log line, just a request that never
completes until Slack's ~3s client timeout gives up (nginx logs this as a
499, since upstream never responded).

Add a post_fork gunicorn hook that drops any DB connections inherited from
the master, forcing each worker to lazily open its own independent
connection on first use. This is the standard fix for gunicorn --preload
combined with persistent database connections (CONN_MAX_AGE=300 here).

Verified locally: gunicorn boots with the new config and post_fork hook
runs without error, and a DB-touching request (/adm/login/) completes in
~20-30ms with no hang. Couldn't reproduce the exact master/worker socket
race end-to-end without MySQL access in this environment, but the
mechanism matches the observed symptoms precisely and this is the
documented remedy for it.
.dockerignore excludes everything by default and only allowlists specific
paths back in. gunicorn.conf.py (added in the previous commit for the
post_fork DB-connection fix) was never added to that allowlist, so it never
made it into the image and script/run's --config gunicorn.conf.py failed
with "Error: 'gunicorn.conf.py' doesn't exist".
The post_fork connections.close_all() from the last commit turned out to
be a no-op for this: Django's ConnectionHandler keys connections by
thread-local storage, and post_fork runs on the thread that called fork()
(the arbiter's), not the Cron thread that actually opened the connection
in the master. So there was nothing in that thread-local slot to close,
and the 499s continued exactly as before.

Re-examining the actual mechanism: --preload runs `simone.urls` (which
starts the singleton Cron thread and, via its first tick, opens a DB
connection inside a transaction.atomic() block) in the gunicorn master
before workers are forked -- confirmed by the container logs, where "Cron
run: starting" always precedes "Booting worker". fork() duplicates
whatever that connection has in flight into the worker. With multi-
workspace support, essentially every incoming Slack event needs to look up
the Workspace row for the team_id, so if the duplicated/corrupted
connection leaves Cron's transaction (and any locks it took) stuck open
with nothing able to ever commit or roll it back, every subsequent request
needing that same table blocks forever waiting on the lock -- nothing
logged, until Slack's ~3s client timeout gives up and nginx records a 499.
100% reproducible, matching what's been observed.

The robust fix is to stop sharing anything across the fork boundary at
all: drop --preload so each worker loads the app (and starts its own Cron
thread, opens its own DB connections) fully independently, post-fork, with
nothing inherited from a master that did real work first. Verified this
reorders app loading to happen after "Booting worker" instead of before,
and a DB-touching request still completes in ~30ms.

This relies on running a single worker (script/run doesn't pass --workers,
so gunicorn defaults to one) so Cron only ever starts once; noted in a
comment.

Also replaced gunicorn.conf.py's now-defunct post_fork hook with a
worker_abort hook that dumps every thread's stack via faulthandler if a
worker ever gets SIGABRT'd for timing out, so a future hang is diagnosable
from the logs directly instead of another guess-and-redeploy cycle.
@ross
ross merged commit 9b8b943 into main Aug 10, 2026
1 check passed
@ross
ross deleted the dependabot/pip/django-5.2.16 branch August 10, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant