Skip to content

fix: use RLock to prevent deadlock in database singleton initialization#654

Merged
njbrake merged 1 commit intomainfrom
fix/rlock-deadlock
Mar 17, 2026
Merged

fix: use RLock to prevent deadlock in database singleton initialization#654
njbrake merged 1 commit intomainfrom
fix/rlock-deadlock

Conversation

@njbrake
Copy link
Collaborator

@njbrake njbrake commented Mar 17, 2026

Description

get_engine() and get_session_factory() in backend/app/database.py share the same threading.Lock(). When SessionLocal() is the first DB call (both singletons uninitialized), get_session_factory() acquires the lock then calls get_engine(), which tries to acquire the same non-reentrant lock, causing a deadlock.

This was masked in the OSS app because _verify_database() in the lifespan always pre-initializes get_engine(), but any consumer that calls SessionLocal() before get_engine() (e.g. clawbolt-premium) deadlocks on startup.

Fix: switch threading.Lock() to threading.RLock() (reentrant) so nested acquisition by the same thread succeeds.

Type

  • Feature
  • Bug fix
  • Refactor
  • Test
  • CI/CD
  • Documentation

Checklist

  • Tests pass (uv run pytest -v)
  • Lint passes (ruff check backend/ && ruff format --check backend/)
  • New tests added for new functionality
  • Bug fixes include regression tests

AI Usage

  • AI-assisted (describe how)
  • No AI used

Claude Code diagnosed the deadlock root cause through systematic middleware/request stack analysis and implemented the fix.

get_engine() and get_session_factory() share the same threading.Lock().
When SessionLocal() is the first DB call (before get_engine() has been
called independently), get_session_factory() acquires the lock then
calls get_engine() which tries to acquire the same non-reentrant lock,
causing a deadlock.

This was masked in the OSS app because _verify_database() in the
lifespan always calls get_engine() first. Premium (and any other
consumer that calls SessionLocal() without a prior get_engine()) hits
the deadlock on startup.

Switching to RLock (reentrant) allows the nested acquisition to succeed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added missing-template PR is missing required template checklist and removed missing-template PR is missing required template checklist labels Mar 17, 2026
@njbrake njbrake merged commit 07ea5f4 into main Mar 17, 2026
10 of 11 checks passed
@njbrake njbrake deleted the fix/rlock-deadlock branch March 17, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant