From 86f2b2b39fffac82886698fdd2dde5ae34a06a50 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 18:20:27 +0000 Subject: [PATCH 1/2] docs: reflect Signal-store read routing from whatsapp-rust#1222 read_pool_size now also governs SqliteStore's own SignalStore/ AppSyncStore/ProtocolStore/DeviceStore reads (session, identity, sender-key, pre-key lookups), not just whatsapp-rust-chat-store's queries. Notes the deliberate exceptions kept on the write queue. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016x8h4pG6KBbRULQ1cBfzen --- api/store.mdx | 2 +- concepts/storage.mdx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/store.mdx b/api/store.mdx index 03181c1..5d4c5f8 100644 --- a/api/store.mdx +++ b/api/store.mdx @@ -516,7 +516,7 @@ impl SharedSqlite { ``` -`read` takes a permit from a separate reader pool, sized by [`SqliteStoreConfig::read_pool_size`](/concepts/storage#memory-and-thread-tuning-sqlitestoreconfig) — so a burst of reads can run alongside a pending write instead of queueing behind it on `run`'s single permit. If `read_pool_size` is `0` (the default) or the underlying connection isn't WAL, `read` falls back to queueing on the same permit as `run` — it's always safe to call. Wrapping the closure in a deferred transaction also means a `read` that issues more than one statement (e.g. resolve a chat's identity keys, then query by them) sees one consistent snapshot across all of them, rather than possibly straddling a write that commits in between. Prefer `read` over `run` for anything that only queries — [`whatsapp-rust-chat-store`](/api/chat-store)'s query paths do. +`read` takes a permit from a separate reader pool, sized by [`SqliteStoreConfig::read_pool_size`](/concepts/storage#memory-and-thread-tuning-sqlitestoreconfig) — so a burst of reads can run alongside a pending write instead of queueing behind it on `run`'s single permit. If `read_pool_size` is `0` (the default) or the underlying connection isn't WAL, `read` falls back to queueing on the same permit as `run` — it's always safe to call. Wrapping the closure in a deferred transaction also means a `read` that issues more than one statement (e.g. resolve a chat's identity keys, then query by them) sees one consistent snapshot across all of them, rather than possibly straddling a write that commits in between. Prefer `read` over `run` for anything that only queries — [`whatsapp-rust-chat-store`](/api/chat-store)'s query paths do, and so does most of `SqliteStore`'s own `SignalStore`/`AppSyncStore`/`ProtocolStore`/`DeviceStore` surface as of [whatsapp-rust#1222](https://github.com/oxidezap/whatsapp-rust/pull/1222) — session, identity, sender-key, and pre-key lookups among them. A handful of reads whose stale answer would go out on the wire or overwrite a cache unconditionally (app-state sync key lookups, `messageSecret` reads, `get_devices`) are deliberately kept on `run` instead. ### Features diff --git a/concepts/storage.mdx b/concepts/storage.mdx index 4b2b636..68d2a67 100644 --- a/concepts/storage.mdx +++ b/concepts/storage.mdx @@ -965,6 +965,10 @@ let backend = Arc::new( `SqliteStore::new` and `SqliteStore::new_for_device` are unchanged — they delegate to `SqliteStoreConfig::default()` internally. Raising `pool_size` above `1` makes *writes* concurrent, which SQLite does not want — two deferred transactions that both read and then write deadlock on the upgrade, and `busy_timeout` cannot break it. `read_pool_size` (set via the `with_read_pool_size` builder, or the struct field directly) is the knob for concurrency instead: it reserves connections purely for reads, which WAL lets run alongside a pending write without contending for the write lock. Left at `0`, reads queue on the same permit as writes exactly as before this knob existed. + +As of [whatsapp-rust#1222](https://github.com/oxidezap/whatsapp-rust/pull/1222), `read_pool_size` also covers most of `SqliteStore`'s own `SignalStore`/`AppSyncStore`/`ProtocolStore`/`DeviceStore` reads, not just [`whatsapp-rust-chat-store`](/api/chat-store)'s queries. Session, identity, sender-key, and pre-key lookups on the decrypt path — `get_session`, `load_identity`, `get_sender_key`, `load_prekey`, and similar — now run on the reader pool instead of queueing behind `pool_size`'s single write permit, raising the ceiling on read concurrency during a write-behind flush. A handful of reads stay on the write queue by design because a stale answer would go out on the wire, fail an operation outright, or get promoted into a cache unconditionally — app-state sync key lookups, `messageSecret` reads, and `get_devices` among them. Widening `read_pool_size` past `0` now buys concurrency for the whole read surface, not just chat/message queries. + + **Multi-session measurements (from PR #926):** | | before | after (default config) | From 1577f3cbff4eda3e0489e3d98c06279639d14ba7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 18:25:28 +0000 Subject: [PATCH 2/2] docs: address review feedback on read-pool routing notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Note stale reads can fail an operation outright (api/store.mdx), matching the same claim already made in concepts/storage.mdx. - Say "write-path permits" instead of "single permit" since pool_size can be configured above its default of 1. - Say "most of the read surface" instead of "the whole read surface" — a handful of reads stay on the write queue by design. - Split the two long notes into shorter, one-idea sentences. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016x8h4pG6KBbRULQ1cBfzen --- api/store.mdx | 2 +- concepts/storage.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/store.mdx b/api/store.mdx index 5d4c5f8..c550e46 100644 --- a/api/store.mdx +++ b/api/store.mdx @@ -516,7 +516,7 @@ impl SharedSqlite { ``` -`read` takes a permit from a separate reader pool, sized by [`SqliteStoreConfig::read_pool_size`](/concepts/storage#memory-and-thread-tuning-sqlitestoreconfig) — so a burst of reads can run alongside a pending write instead of queueing behind it on `run`'s single permit. If `read_pool_size` is `0` (the default) or the underlying connection isn't WAL, `read` falls back to queueing on the same permit as `run` — it's always safe to call. Wrapping the closure in a deferred transaction also means a `read` that issues more than one statement (e.g. resolve a chat's identity keys, then query by them) sees one consistent snapshot across all of them, rather than possibly straddling a write that commits in between. Prefer `read` over `run` for anything that only queries — [`whatsapp-rust-chat-store`](/api/chat-store)'s query paths do, and so does most of `SqliteStore`'s own `SignalStore`/`AppSyncStore`/`ProtocolStore`/`DeviceStore` surface as of [whatsapp-rust#1222](https://github.com/oxidezap/whatsapp-rust/pull/1222) — session, identity, sender-key, and pre-key lookups among them. A handful of reads whose stale answer would go out on the wire or overwrite a cache unconditionally (app-state sync key lookups, `messageSecret` reads, `get_devices`) are deliberately kept on `run` instead. +`read` takes a permit from a separate reader pool, sized by [`SqliteStoreConfig::read_pool_size`](/concepts/storage#memory-and-thread-tuning-sqlitestoreconfig). A burst of reads can then run alongside a pending write instead of queueing behind `run`'s write-path permits. If `read_pool_size` is `0` (the default) or the underlying connection isn't WAL, `read` falls back to queueing on the same permits as `run` — it's always safe to call. Wrapping the closure in a deferred transaction also means a `read` that issues more than one statement (e.g. resolve a chat's identity keys, then query by them) sees one consistent snapshot across all of them, rather than possibly straddling a write that commits in between. Prefer `read` over `run` for anything that only queries. [`whatsapp-rust-chat-store`](/api/chat-store)'s query paths do, and so does most of `SqliteStore`'s own `SignalStore`/`AppSyncStore`/`ProtocolStore`/`DeviceStore` surface as of [whatsapp-rust#1222](https://github.com/oxidezap/whatsapp-rust/pull/1222) — session, identity, sender-key, and pre-key lookups among them. A handful of reads are deliberately kept on `run` instead: a stale answer for these would go out on the wire, fail an operation outright, or overwrite a cache unconditionally (app-state sync key lookups, `messageSecret` reads, `get_devices`). ### Features diff --git a/concepts/storage.mdx b/concepts/storage.mdx index 68d2a67..6e77c0f 100644 --- a/concepts/storage.mdx +++ b/concepts/storage.mdx @@ -966,7 +966,7 @@ let backend = Arc::new( -As of [whatsapp-rust#1222](https://github.com/oxidezap/whatsapp-rust/pull/1222), `read_pool_size` also covers most of `SqliteStore`'s own `SignalStore`/`AppSyncStore`/`ProtocolStore`/`DeviceStore` reads, not just [`whatsapp-rust-chat-store`](/api/chat-store)'s queries. Session, identity, sender-key, and pre-key lookups on the decrypt path — `get_session`, `load_identity`, `get_sender_key`, `load_prekey`, and similar — now run on the reader pool instead of queueing behind `pool_size`'s single write permit, raising the ceiling on read concurrency during a write-behind flush. A handful of reads stay on the write queue by design because a stale answer would go out on the wire, fail an operation outright, or get promoted into a cache unconditionally — app-state sync key lookups, `messageSecret` reads, and `get_devices` among them. Widening `read_pool_size` past `0` now buys concurrency for the whole read surface, not just chat/message queries. +As of [whatsapp-rust#1222](https://github.com/oxidezap/whatsapp-rust/pull/1222), `read_pool_size` also covers most of `SqliteStore`'s own `SignalStore`/`AppSyncStore`/`ProtocolStore`/`DeviceStore` reads, not just [`whatsapp-rust-chat-store`](/api/chat-store)'s queries. Session, identity, sender-key, and pre-key lookups on the decrypt path — `get_session`, `load_identity`, `get_sender_key`, `load_prekey`, and similar — now run on the reader pool. Before this change they queued behind `pool_size`'s write-path permits even with `read_pool_size` set, so this raises the ceiling on read concurrency during a write-behind flush. A handful of reads stay on the write queue by design: app-state sync key lookups, `messageSecret` reads, and `get_devices` among them, since a stale answer for these would go out on the wire, fail an operation outright, or get promoted into a cache unconditionally. Widening `read_pool_size` past `0` now buys concurrency for most of the read surface, not just chat/message queries. **Multi-session measurements (from PR #926):**