fix(tasks): use task_session() in all Celery task bodies (#129) #159

Merged
claude-bot merged 1 commit from fix/129-celery-task-session into main 2026-07-15 22:46:01 +00:00
Contributor

Summary

Celery spins up a fresh event loop per task invocation, so a pooled connection from the global AsyncSessionLocal can be reused under a different loop than it was created on, making asyncpg raise Future ... attached to a different loop. In production this presented as intermittent recap-email and vote-auto-close failures that cleared after a worker restart.

app/database.py already provides task_session() — a throw-away NullPool engine per call — and most tasks use it. This sweeps the remaining offenders to use it too.

Changes

  • Convert every AsyncSessionLocal use inside a Celery task body to task_session() across app/tasks/reminder_tasks.py and app/tasks/planning_tasks.py (7 + 1 sites). Both context managers have identical commit semantics (neither auto-commits), so it is a drop-in swap.
  • Add tests/test_task_session_hygiene.py: a regression guard that fails if AsyncSessionLocal is referenced anywhere under app/tasks/.

Verification

  • New guard test + all touched task test modules pass (35 tests).
  • Full backend suite green: 359 passed locally against the test DB.

Closes #129

🤖 Generated with Claude Code

## Summary Celery spins up a fresh event loop per task invocation, so a pooled connection from the global `AsyncSessionLocal` can be reused under a different loop than it was created on, making asyncpg raise `Future ... attached to a different loop`. In production this presented as intermittent recap-email and vote-auto-close failures that cleared after a worker restart. `app/database.py` already provides `task_session()` — a throw-away NullPool engine per call — and most tasks use it. This sweeps the remaining offenders to use it too. ## Changes - Convert every `AsyncSessionLocal` use inside a Celery task body to `task_session()` across `app/tasks/reminder_tasks.py` and `app/tasks/planning_tasks.py` (7 + 1 sites). Both context managers have identical commit semantics (neither auto-commits), so it is a drop-in swap. - Add `tests/test_task_session_hygiene.py`: a regression guard that fails if `AsyncSessionLocal` is referenced anywhere under `app/tasks/`. ## Verification - New guard test + all touched task test modules pass (35 tests). - Full backend suite green: **359 passed** locally against the test DB. Closes #129 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(tasks): use task_session() (NullPool) in all Celery task bodies
All checks were successful
CI / Docker image build (pull_request) Successful in 27s
CI / Backend lint (ruff) (pull_request) Successful in 48s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m14s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Backend migration, tests, and audit (pull_request) Successful in 3m26s
e6b7181f64
Celery spins up a fresh event loop per task invocation, so a pooled
connection from the global AsyncSessionLocal can be reused under a
different loop than it was created on, making asyncpg raise
"Future ... attached to a different loop". This surfaced as intermittent
recap-email and vote-auto-close failures that cleared after a worker
restart.

Sweep every task body under app/tasks/ to use task_session() (a
throw-away NullPool engine per call) instead of the pooled
AsyncSessionLocal. Both context managers have identical commit
semantics (neither auto-commits), so this is a drop-in swap.

Adds a regression guard (test_task_session_hygiene) that fails if
AsyncSessionLocal is referenced anywhere under app/tasks/.

Closes #129

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-07-15 22:42:50 +00:00
claude-bot deleted branch fix/129-celery-task-session 2026-07-15 22:46:01 +00:00
Sign in to join this conversation.
No description provided.