From a5b3c8263e68fa2db9df5b12e75a82b10919a350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:01:24 -0300 Subject: [PATCH 1/4] docs: reflect active chat lane eviction fix (whatsapp-rust#1045) Document that per-chat lanes now survive capacity eviction while a message is in flight (evict_guard + QueuedChatMessage.lane_liveness), and note that this closes the specific chat-lane-eviction trigger previously described for the sender-key chain lock section. --- concepts/architecture.mdx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index 3233e38b..d26a9963 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -538,9 +538,13 @@ Prevents race conditions where a later message is processed before the PreKey me ```rust pub(crate) chat_lanes: Cache, // ChatLane { enqueue_lock, queue_tx } -// Each queue: async_channel::bounded::>(500) +// Each queue: async_channel::bounded::(500) ``` + +**Active lanes survive capacity eviction (v0.6).** Every queued item is a `QueuedChatMessage { node, lane_liveness }`, where `lane_liveness` is a clone of the lane's `enqueue_lock`. The cache's `evict_guard` refuses to evict a lane while any in-flight message still holds that clone (`Arc::strong_count(&lane.enqueue_lock) > 1`); the worker drops its copy only after it finishes processing that message. Previously, a lane could be capacity-evicted right after its worker dequeued a message, and a later stanza for the same chat would then miss the cache and spawn a second worker — letting two workers process the same chat concurrently and out of order. Idle lanes (no in-flight message) remain evictable exactly as before. + + ### Per-device session locks Prevents concurrent Signal protocol operations on the same session. Each device JID gets its own lock, keyed by protocol address strings generated by `to_protocol_address_string()` (format: `user[:device]@server.0`): @@ -575,6 +579,10 @@ let decrypt_result = group_decrypt(ciphertext, &mut adapter.sender_key_store, &s Without this lock, two decrypt workers for the same `(group, sender)` — reachable when a [chat lane](#per-chat-lanes) is capacity-evicted while its worker is still draining, and a later stanza for that chat misses the cache and spawns a second worker at the same connection generation — could both load the sender-key chain, advance it, and store their result, with the last store silently winning and dropping a chain step. This mirrors the per-device session lock the 1:1 send/receive paths already hold around their Signal ratchet mutations. + +**Chat-lane eviction trigger closed (v0.6).** The specific double-worker path described above — a chat lane evicted while its worker was still draining — is now closed by the [active-lane eviction guard](#per-chat-lanes): a lane with an in-flight message can no longer be capacity-evicted, so a later stanza for that chat can no longer spawn a second worker. This chain lock remains in place as defense-in-depth against any other path that could produce two concurrent decrypt workers for the same `(group, sender)`. + + ### Background Saver Periodic persistence with dirty flag optimization: From 4e6c7df014e74e2b8c894f881bc568421fda4345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:15:53 -0300 Subject: [PATCH 2/4] docs: fix chat lane queue as unbounded, not capacity-500 The queue element type comment was updated in the previous commit but still claimed a bounded(500) channel with per-lane backpressure. The actual code (handlers/message.rs in whatsapp-rust) uses async_channel::unbounded. The real capacity limit is on the number of cached lanes (chat_lanes_capacity, default 5,000 per cache_config.rs), not on messages per lane. Addresses review feedback from @chatgpt-codex-connector on #410. --- concepts/architecture.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index d26a9963..116341e7 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -533,12 +533,12 @@ For bots that don't need chat history, `skip_history_sync()` sends a receipt so ### Per-Chat Lanes -Prevents race conditions where a later message is processed before the PreKey message. Each chat gets a lane combining an enqueue lock and a bounded channel (capacity **500** messages) into a single cached entry, providing backpressure to prevent memory amplification when many chats are active simultaneously: +Prevents race conditions where a later message is processed before the PreKey message. Each chat gets a lane combining an enqueue lock and an **unbounded** channel into a single cached entry. Backpressure comes from capping the number of cached lanes rather than messages within a lane: the `chat_lanes` cache itself has a capacity (`chat_lanes_capacity`, default **5,000**) and evicts idle lanes once full — see the note below for what counts as idle: ```rust pub(crate) chat_lanes: Cache, // ChatLane { enqueue_lock, queue_tx } -// Each queue: async_channel::bounded::(500) +// Each queue: async_channel::unbounded::() ``` From 6601273be3aace5dad72a9e65296b564e9158496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:24:30 -0300 Subject: [PATCH 3/4] docs: clarify chat_lanes_capacity is a lane-count soft cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit review feedback on #410: this file's description of chat_lanes_capacity conflicted with concepts/architecture.mdx's updated wording. Aligns both to describe it as the number of cached per-chat lanes (each pairing an enqueue lock with an unbounded queue), and notes it's a soft cap like the other coordination caches — an active lane is never evicted, so the map can briefly exceed capacity when every cached lane is in use. --- api/bot.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/bot.mdx b/api/bot.mdx index 0c33ff6a..5a7b9adb 100644 --- a/api/bot.mdx +++ b/api/bot.mdx @@ -1080,7 +1080,7 @@ The `recent_messages` cache is disabled by default (capacity 0), meaning sent me | Setting | Default | Description | |---------|---------|-------------| | `session_locks_capacity` | 10,000 | Per-device Signal session lock capacity. Soft cap: a lock a task is actively holding is never evicted, so the map can briefly exceed this under heavy concurrent fan-out (bounded by the number of concurrently-held locks) rather than evicting a live lock and letting two writers race the same session | -| `chat_lanes_capacity` | 5,000 | Per-chat lane capacity (combined enqueue lock + message queue) | +| `chat_lanes_capacity` | 5,000 | Per-chat lane capacity: number of cached lanes, each an enqueue lock paired with an **unbounded** message queue (there is no per-lane message cap). Soft cap: a lane with an in-flight message is never evicted, so the map can briefly exceed this under a backlog spread across many active chats, rather than evicting a live lane and letting a second worker start on the same chat — see [Concurrency Patterns — Per-Chat Lanes](/concepts/architecture#per-chat-lanes) | | `group_distribution_locks_capacity` | 512 | Per-group sender-key distribution lane capacity. Soft cap: a live lane (held by an in-flight SKDM fan-out, rotation, or tracker reset) is never evicted, so the map can briefly exceed this under concurrent fan-out rather than breaking tracker ordering | From b48d555216b5cd29048ff1d012c6dd7f2f6aaa3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:26:59 -0300 Subject: [PATCH 4/4] docs: clarify chat_lanes_capacity is a soft cap Addresses CodeRabbit review feedback on #410: a lane with an in-flight message is protected from eviction, so the chat_lanes cache can briefly exceed chat_lanes_capacity when every cached lane happens to be active at once, rather than evicting a live lane. Mirrors the phrasing already used for session_locks_capacity/group_distribution_locks_capacity. --- concepts/architecture.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index 116341e7..5854ee2e 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -533,7 +533,7 @@ For bots that don't need chat history, `skip_history_sync()` sends a receipt so ### Per-Chat Lanes -Prevents race conditions where a later message is processed before the PreKey message. Each chat gets a lane combining an enqueue lock and an **unbounded** channel into a single cached entry. Backpressure comes from capping the number of cached lanes rather than messages within a lane: the `chat_lanes` cache itself has a capacity (`chat_lanes_capacity`, default **5,000**) and evicts idle lanes once full — see the note below for what counts as idle: +Prevents race conditions where a later message is processed before the PreKey message. Each chat gets a lane combining an enqueue lock and an **unbounded** channel into a single cached entry. Backpressure comes from capping the number of cached lanes rather than messages within a lane: the `chat_lanes` cache itself has a capacity (`chat_lanes_capacity`, default **5,000**) and evicts idle lanes once full. This is a soft cap — a lane with an in-flight message is protected from eviction (see the note below), so if every cached lane happens to be active at once, the map can briefly exceed capacity rather than evicting a live lane and letting a second worker start on the same chat: ```rust pub(crate) chat_lanes: Cache,