Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,15 @@ The group send path no longer serializes encryption behind a client-level lock.
This is an internal performance change — no public method on `Client::send_message` was renamed, and the order of `<to>` children in the resulting stanza is unchanged. If you implemented a custom `SignalStore`, note that `update_device_lists(records: Vec<DeviceListRecord>)` is now part of the trait so the fan-out can batch its writes.

<Note>
While *per-device* encryption runs concurrently, the sender-key chain itself is serialized **per `(group, sender)` pair** (v0.6). SKDM creation and `skmsg` encryption on the same chain can't race, so two concurrent sends to the same group ratchet the sender key deterministically instead of corrupting the chain. Different groups (or different senders) still encrypt fully in parallel.
While *per-device* encryption runs concurrently, the sender-key chain is protected by **two separate locks per `(group, sender)` pair**:

1. **Session-setup lock** (`SenderKeyStore::session_setup_lock`) — held only across `ensure_sessions_for_devices` (prekey fetch + X3DH). May span network I/O. Warm sends (no SKDM needed) never take it, so they are never blocked by a cold send's network round-trip.

2. **Chain lock** (`SenderKeyStore::sender_key_lock`) — held across SKDM creation + pairwise encrypt fan-out + `skmsg` encrypt. Pure CPU; never spans network I/O. This is the invariant that prevents two concurrent sends from splitting the key between the SKDM and the `skmsg`.

Prior to [#807](https://github.com/oxidezap/whatsapp-rust/pull/807), a single chain lock covered both phases, causing concurrent group sends to serialize behind a server round-trip whenever a new session needed to be established. Now only the CPU phase is in the critical section. Different groups (or different senders) encrypt fully in parallel, unchanged.

`encrypt_for_devices` is composed of two public halves: `ensure_sessions_for_devices` (network, returns `SessionPlan`) and `encrypt_for_devices_with_sessions` (CPU, consumes `SessionPlan`). The DM path calls `encrypt_for_devices` unchanged; the group path calls them separately with the chain lock taken only around the second.
</Note>

### In-memory sender key device cache
Expand Down
31 changes: 31 additions & 0 deletions changelog/2026-06-09-group-send-perf.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: "June 9, 2026 — Group send: session setup hoisted out of chain lock"
description: "Concurrent sends to the same group no longer serialize behind prekey fetch I/O. The sender-key chain lock now covers only the CPU-bound SKDM creation and skmsg encrypt. A new per-group session-setup lock serializes cold prekey fetch and X3DH without blocking warm sends."
---

## Performance

**Group send: session setup hoisted out of the sender-key chain lock ([#807](https://github.com/oxidezap/whatsapp-rust/pull/807))**

Previously, `prepare_group_stanza` took the per-(group, sender) chain lock at the top and held it through the entire SKDM path — including cold-path device resolution and prekey fetch + X3DH session establishment. Any group send that needed to establish a new pairwise session held the chain lock across a server round-trip, so concurrent sends to the same group serialized behind that RTT.

**Two-phase split.** `encrypt_for_devices` is now composed of two separately callable halves:

- **`ensure_sessions_for_devices`** — network phase: LID-first session lookup, batch prekey fetch, parallel X3DH. Returns a `SessionPlan` (per-device LID overrides + 406 flag). Touches only session/identity state, never a sender-key chain. May span network I/O.
- **`encrypt_for_devices_with_sessions`** — CPU phase: the bounded pairwise encrypt fan-out, consuming the `SessionPlan`. Safe to run under a lock that must not span I/O.

`encrypt_for_devices` remains as the composition of both, so the DM path is unchanged.

The group path now runs `ensure_sessions_for_devices` before taking the chain lock; the chain lock covers only SKDM creation + pairwise fan-out + `skmsg` encrypt. This matches WA Web, where `ensureE2ESessions` is a separate step before `GroupSkmsgJob`'s encrypt.

**Session-setup lock.** Hoisting session setup out of the chain lock would have let two concurrent cold sends to the same group race prekey fetch + X3DH writes to the same per-device sessions. A new `SenderKeyStore::session_setup_lock` (per-group, default-uncontended; backed by the same `SignalStoreCache` lock map under a `::setup` key suffix) is held only during `ensure_sessions_for_devices`. Same-group cold sends serialize their setup exactly as before; warm sends (no SKDM needed) never take it, so the chain lock stays network-free.

**New public items:**
- `wacore::send::encrypt::ensure_sessions_for_devices`
- `wacore::send::encrypt::encrypt_for_devices_with_sessions`
- `wacore::send::encrypt::SessionPlan`
- `SenderKeyStore::session_setup_lock` — defaulted trait method (returns a fresh uncontended mutex by default); production stores override via `SignalStoreCache::session_setup_lock`

**Tracing.** Both the DM and group paths now emit `wa.send.ensure_sessions` + `wa.send.encrypt_fanout` instead of a single combined `wa.send.encrypt_fanout` span.

**No breaking changes** — `encrypt_for_devices` and `prepare_group_stanza` keep their existing signatures.
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"changelog/2026-06-09-device-list-remove",
"changelog/2026-06-09-usync-empty-record",
"changelog/2026-06-09-retry-resync",
"changelog/2026-06-09-group-send-perf",
"changelog/2026-06-08",
"changelog/2026-06-06",
"changelog/2026-06-05-abprops",
Expand Down