Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

104 lines
4.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0004: QueuePool (not NullPool) for SQLCipher databases
**Date:** 2026-04-13
**Status:** Accepted
## Context
Each user has their own SQLCipher-encrypted SQLite database, opened at
login and closed at logout. Background threads (research workers,
metric writers, news scheduler jobs) need database sessions for the
same user concurrently with Flask request handlers.
### Why QueuePool
SQLCipher's `PRAGMA key` adds ~0.2 ms per connection open. With 2030
queries per page load, NullPool (new connection per checkout) adds a
noticeable 46 ms overhead vs QueuePool's ~1.5 ms for pool-resident
connections.
### Why not per-thread NullPool engines
An earlier design maintained a second engine system: one NullPool
engine per `(username, thread_id)` in `_thread_engines`, used by
background threads for metric writes. This was removed in PR #3441
because:
1. **FD leak.** Each SQLCipher + WAL connection holds 3 file
descriptors (main db + WAL + SHM). Orphaned thread engines — left
behind when `@thread_cleanup` did not fire — accumulated FDs
unboundedly, eventually exhausting the 1024 soft limit and crashing
the server with `OSError: [Errno 24] Too many open files`.
2. **Architectural redundancy.** The per-user QueuePool engine is
already created with `check_same_thread=False`, making it safe for
background threads. Routing all work through one bounded pool per
user keeps FD usage at `pool_size + max_overflow` (currently 60)
instead of scaling with background-thread count.
### SQLCipher + WAL handle leak workaround
SQLCipher in WAL mode leaks file handles when pooled connections close
out of open-order (a known issue with WAL-mode SQLite engines under
connection pooling). The cleanup scheduler in `connection_cleanup.py`
calls `engine.dispose()` on all per-user engines every 30 minutes,
closing all idle pooled connections and resetting handle state. This is
a workaround, not a root fix — it limits accumulation to a 30-minute
window.
### Current pool sizing
```
pool_size = 20
max_overflow = 40
pool_timeout = 10 # seconds; fail fast rather than queue
pool_recycle = 3600 # seconds; recycle stale connections
pool_pre_ping = True
```
Peak FD usage per user: `(20 + 40) × 2 + 1 = 121` (WAL mode).
## Decision
Use a single shared QueuePool engine per user for all threads (request
handlers and background workers). Do not maintain per-thread engines.
Periodic `dispose()` mitigates the SQLCipher+WAL handle leak.
## Consequences
- FD usage is bounded and predictable.
- `pool_timeout=10` makes pool exhaustion a loud error rather than a
silent deadlock.
- The `ParallelConstrainedStrategy` (`max_workers=100`) could
theoretically spike past 60 simultaneous checkouts. Sessions are
short-lived (millisecond metric writes), so sustained contention is
unlikely. Flagged as a known follow-up.
## Addendum — PR #3487 investigation (2026-04-16)
PR #3487 proposed skipping the 30-min `engine.dispose()` for any engine
with `pool.checkedout() > 0`, citing a claim that dispose orphans
checked-out connections and causes torn writes in the post-login bulk
settings import. Investigation found:
- **SA 2.0 source disagrees.** `QueuePool.dispose` only drains idle
queue entries (`_pool.get(False)`); `Engine.dispose` replaces the
pool via `pool.recreate()`. SA docs: *"Connections that are still
checked out will not be closed."* A thread holding a checked-out
connection keeps using it until return.
- **Real root cause was elsewhere.** The sticky-loop symptom came
from the post-login path committing twice
(`load_from_defaults_file(commit=True)` then `update_db_version()`
with its own commit), which left `app.version` unwritten on any
inter-commit failure. Fix: one session, one terminal commit, both
calls use `commit=False`. See `web/auth/routes.py` ATOMICITY
INVARIANT comment.
- **Regression guard:** `tests/database/test_post_login_settings_atomicity.py`
locks in both properties — the atomic all-or-nothing write and the
SA 2.0 checked-out-survives-dispose contract — so neither can
silently regress.
Future refactors of the cleanup cycle should preserve the
checked-out-survival property; do not add a `checkedout()` skip guard
without a real reproducer.