Share DB connection pragmas across the pool, and wait for DB locks#186
Share DB connection pragmas across the pool, and wait for DB locks#186ara4n wants to merge 2 commits into
Conversation
in an attempt to reduce db locks. Also, wait 5s for a lock if needed.
dd92c4d to
b3d6b5f
Compare
`get_connection()` ran `Database::unlock()` -> `PRAGMA key` on every pool checkout, and r2d2 reuses physical connections, so the key was re-derived on every read. SQLCipher's `PRAGMA key` runs PBKDF2 (~256k iterations, ~20ms), so the startup reconciliation scan - one `get_connection` per `isRoomIndexed` across thousands of rooms - spent ~14s of CPU (24% of a core for the first minute) re-deriving the same key hundreds of times. Move the keying into the r2d2 connection manager's `with_init` hook (`build_manager`), which runs once per physical connection, alongside the cheap per-connection pragmas. `get_connection`/`new_with_config`/recovery no longer re-key on checkout. `unlock` becomes `verify_unlocked`, which only confirms the (already-keyed) connection decrypts, without the expensive `PRAGMA key`. The rare cipher-migration path still runs the required "PRAGMA key; PRAGMA cipher_migrate" sequence on a bare connection. Profiling confirms kdf_pbkdf2_derive drops from ~14.4s to 0 at launch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP4yoxKeGLRDLkCPTu3DFq
b3d6b5f to
5d0211c
Compare
There was a problem hiding this comment.
Seems plausible in principle, but there's a bunch of stuff that needs cleaning up, please.
On the subject of the commit messages:
Claude-Session: https://claude.ai/code/session_01JP4yoxKeGLRDLkCPTu3DFq
should this link do something useful?
unlockbecomesverify_unlocked, which only confirms the (already-keyed) connection decrypts,
I can't find a verify_unlocked ?
| /// source of "database is locked" errors (every search / isRoomIndexed | ||
| /// would briefly fight the writer). `journal_mode=WAL` is persistent in the | ||
| /// database file, so it only needs to be set once - see `init_pragmas`. | ||
| fn set_pragmas(connection: &rusqlite::Connection) -> Result<()> { |
There was a problem hiding this comment.
could we maybe give set_pragmas and init_pragmas better names, which better reflect how they are used? I'm slightly strugging for good names, but maybe set_per_connection_pragmas and set_global_pragmas?
| /// One-time, database-level initialisation run once at startup on the writer | ||
| /// connection. These operations take a database lock, so they must NOT run | ||
| /// on every connection acquisition. `journal_mode=WAL` is persistent in the | ||
| /// database file (it applies to all subsequent connections automatically), | ||
| /// and the WAL is truncated once here rather than on every read. | ||
| fn init_pragmas(connection: &rusqlite::Connection) -> Result<()> { | ||
| connection.pragma_update(None, "journal_mode", "WAL")?; | ||
| connection.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
I'd actually be tempted to inline this, rather than have a separate method. It's only two lines, and it's only called in one place.
| /// that are cheap and take no database lock, so they must run on every | ||
| /// connection (the writer and every pooled reader acquired via | ||
| /// `get_connection`). | ||
| /// |
There was a problem hiding this comment.
These are connection-scoped settings that are cheap and take no database lock, so they must run on every connection
Why does the fact they are cheap mean that they have to be run on every connection?
There was a problem hiding this comment.
well, this seems to have been addressed in the second commit :-S
| /// Note: this deliberately does NOT set `journal_mode` or run | ||
| /// `wal_checkpoint`. Both of those take a database-level lock and contend | ||
| /// with the writer; running them on every reader acquisition was a major | ||
| /// source of "database is locked" errors (every search / isRoomIndexed | ||
| /// would briefly fight the writer). `journal_mode=WAL` is persistent in the | ||
| /// database file, so it only needs to be set once - see `init_pragmas`. |
There was a problem hiding this comment.
This feels unnecessarily verbose for the doc-comment of this method. I think I'd just get rid of it.
| // Connections from the pool are already keyed and have their per-connection | ||
| // pragmas applied by the manager's init hook (see `build_manager`). |
There was a problem hiding this comment.
I don't think we need a comment like this (or similar) every time we get a connection from the pool.
| // Ok, let's see if the unlock of the connection failed because of new default | ||
| // settings for the cipher settings, let's see if we can migrate the cipher settings. | ||
| // Take a look at the documentation of cipher_migrate[1] for more info. | ||
| // | ||
| // [1]: https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_migrate |
There was a problem hiding this comment.
It would be nice to retain at least the link to the cipher_migrate doc.
| return Ok(()); | ||
| } | ||
|
|
||
| // Otherwise it was created with older cipher defaults; migrate it in place. |
There was a problem hiding this comment.
| // Otherwise it was created with older cipher defaults; migrate it in place. | |
| // Otherwise it may have been created with an older SQLCipher version; try migrating it. |
| let bare = r2d2::Pool::new(SqliteConnectionManager::file(db_path))?; | ||
|
|
||
| // Probe on one connection: key it and see if it decrypts the database. | ||
| let probe = bare.get()?; |
There was a problem hiding this comment.
To me, a "bare connection" would imply not going via a pool. Indeed, AFAICT we never reuse connections from this pool. Can't we get rid of the pool here?
| let probe = bare.get()?; | |
| let probe = rusqlite::Connection::open(db_path)?; |
| // connection. The `probe` connection is still checked out, so this is a | ||
| // distinct physical connection. cipher_migrate returns "0" on success. | ||
| let connection = bare.get()?; |
There was a problem hiding this comment.
| // connection. The `probe` connection is still checked out, so this is a | |
| // distinct physical connection. cipher_migrate returns "0" on success. | |
| let connection = bare.get()?; | |
| // connection. | |
| let connection = rusqlite::Connection::open(db_path)?; |
| // The cipher_migrate pragma returns a single row/column with the value set to `0` | ||
| // if we succeeded. |
Fixes bits of element-hq/element-web#32119