Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion api/bot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

<Note>
Expand Down
12 changes: 10 additions & 2 deletions concepts/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -533,14 +533,18 @@ 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:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

```rust
pub(crate) chat_lanes: Cache<Jid, ChatLane>,
// ChatLane { enqueue_lock, queue_tx }
// Each queue: async_channel::bounded::<Arc<OwnedNodeRef>>(500)
// Each queue: async_channel::unbounded::<QueuedChatMessage>()
```

<Note>
**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.
</Note>

### 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`):
Expand Down Expand Up @@ -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.

<Note>
**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)`.
</Note>

### Background Saver

Periodic persistence with dirty flag optimization:
Expand Down