Skip to content

Tags: dj-bolt/django-bolt

Tags

v0.11.0

Toggle v0.11.0's commit message
Bump version to 0.11.0

v0.10.3

Toggle v0.10.3's commit message
Bump version to 0.10.3

bolt-mcp-v0.2.3

Toggle bolt-mcp-v0.2.3's commit message
bolt-mcp v0.2.3

v0.10.2

Toggle v0.10.2's commit message
Bump version to 0.10.2

bolt-mcp-v0.2.2

Toggle bolt-mcp-v0.2.2's commit message
bolt-mcp v0.2.2

bolt-mcp-v0.2.1

Toggle bolt-mcp-v0.2.1's commit message
bolt-mcp 0.2.1: OAuth discovery routes are public under global guards (

…#296)

v0.10.1

Toggle v0.10.1's commit message
Bump version to 0.10.1

v0.10.0

Toggle v0.10.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(auth): load request.user on the bounded ORM pool (#275)

* fix(auth): load request.user on the bounded ORM pool

`user_loader` built its own 4-thread pool and ran Django queries on it,
so every `request.user` resolution in an async handler escaped the
framework's ORM connection budget.

That budget is not incidental. `concurrency._get_orm_executor()` is
vendor-aware precisely because SQLite throughput scales inversely with
connection count — the module documents a measured ~2.5x regression from
a wide pool contending on the database file lock — and each pool thread
holds its own long-lived connection. A private pool opens connections
outside that accounting and ignores `DJANGO_BOLT_ORM_THREADS`, which
users set expecting it to cover their queries.

User loading now goes through the shared ORM pool:

- `concurrency` grows `get_orm_executor()`, a sync accessor for callers
  that must block rather than await. `request.user` is a
  `SimpleLazyObject`, so it is forced from code that cannot await.
- Contextvars are carried across the hand-off, matching
  `run_in_orm_executor`, so a request-scoped database router stays
  visible while the user loads.
- The eager `load_user` path moves off `asyncio.to_thread` (the generic
  default pool) onto `run_in_orm_executor` for the same reason.

Consolidating introduces a reentrancy hazard the separate pool masked: a
lazy user forced from work already running on the ORM pool would submit
back into it and wait on a slot the caller itself holds — with the
SQLite default of one thread, forever. `in_orm_executor_thread()` marks
pool workers via the executor's initializer, and a load already on one
runs inline.

Tests cover both loader paths, the thread budget under
DJANGO_BOLT_ORM_THREADS=1, and the reentrant case. The reentrancy test
asserts thread identity rather than blocking on a timeout: a regression
there deadlocks, and a test that hangs CI is worse than one that fails.

* fix(concurrency): synchronize ORM pool creation and guard reentry at the pool

Two defects in the consolidation, both introduced by widening who can reach
the ORM pool.

Lazy construction was check-then-create with no synchronization. That was
safe while `run_in_orm_executor` was the only caller, since it ran on the
event loop thread. User loading forces `request.user` from arbitrary
threads, so two racing first callers could each build a pool — and the
loser's four threads, holding four database connections, would leak outside
the budget this change exists to enforce.

The reentrancy guard sat in the user loader, one level too high.
`load_via_async` drives a custom async `get_user` with `asyncio.run` on a
pool worker, so any ORM work that coroutine awaits re-enters
`run_in_orm_executor` from inside the pool and submits back into it —
the same deadlock, one frame down. The guard now lives in
`run_in_orm_executor` itself, which is where the hand-off happens.

The race test widens the construction window deliberately. Unsynchronized
lazy init is a race whether or not CPython happens to switch threads inside
those few bytecodes; without the widening the test passed against the
unlocked version on every run, which would have made it worthless.

* fix(concurrency): keep async get_user off the ORM pool and fix reentrant sync-ORM breakage

Review follow-up, three connected fixes:

1. run_in_orm_executor's inline reentry branch was broken for real ORM
   work: it is only ever reached under load_via_async's asyncio.run, so
   the pool worker has a running event loop and Django raises
   SynchronousOnlyOperation on any sync query. The existing test passed
   because its callable only read the thread name. Reentrant ORM work now
   crosses to the generic default pool — the worker's own slot is held
   and its thread cannot host sync ORM, so a transient out-of-budget
   connection on this rare path is the only correct option.

2. The asyncio.run shim for a custom async get_user is hosted on the
   default pool instead of an ORM slot. The coroutine may await
   non-database work (an external identity provider); pinning a bounded
   ORM slot for its full lifetime queues every QuerySet evaluation
   behind it — on the SQLite one-thread default, all of them. When the
   load is forced from an ORM worker the shim still runs inline there,
   otherwise the coroutine's own ORM hand-off would deadlock against the
   slot the caller holds.

3. The blocking hand-off logic moves from user_loader into
   concurrency.run_orm_blocking, replacing the raw get_orm_executor
   accessor: one place owns the guard + contextvars dance, and no caller
   can submit past the reentrancy guard. The inline branch now runs in a
   copied context so ContextVar writes stay scoped identically on both
   branches, and an invalid DJANGO_BOLT_ORM_THREADS value is reported
   instead of silently replaced with the default.

Test fixture drains every pool it builds with shutdown(wait=True) before
the transactional teardown truncates tables, and the parked user load
asserts on release.wait instead of discarding a timeout.

* fix(imports): reorder user_loader imports for clarity

* fix(concurrency): synchronize default-pool lazy construction

The user-load shim and the reentrant ORM hand-off made
_get_default_executor() reachable from arbitrary threads; its
check-then-create had the same first-use race the ORM pool's did, where
the losing builder leaks a full pool of threads. Same double-checked
lock, and an invalid DJANGO_BOLT_EXECUTOR_THREADS value is now reported
instead of silently replaced, matching the ORM pool.

---------

Co-authored-by: yeakiniqra <developers@telegramsignalcopier.com>
Co-authored-by: Farhan <www.mfarvirus@gmail.com>

bolt-mcp-v0.2.0

Toggle bolt-mcp-v0.2.0's commit message
bolt-mcp v0.2.0