Skip to content

perf(client): hoist write-once state out of async mutexes - #1227

Merged
jlucaso1 merged 6 commits into
mainfrom
perf/write-once-cells
Aug 7, 2026
Merged

perf(client): hoist write-once state out of async mutexes#1227
jlucaso1 merged 6 commits into
mainfrom
perf/write-once-cells

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Six pieces of client state shared one shape: an async_lock guard whose critical section never awaits, and which in most cases is written once and thereafter only read. That shape costs a future poll and a waiter-list touch per read, and it hides the one thing you actually want the compiler to enforce, which is that nobody holds the guard across an .await.

I checked each claim before touching it, and the rule I applied was that a cell only becomes a OnceLock if it is provably never rewritten. group_cache and app_state_processor qualify: their only writes are the initial None at construction and the getter's fill, and the reconnect cleanup clears the app-state processor's key cache in place rather than swapping the processor. noise_socket does not qualify, since it is replaced per connection, so it only drops to a std::sync::Mutex.

All of this is easy to undo: every change is a lock-type swap plus the .await removals that follow from it, with no ownership moved, no channel introduced and no init relocated. Nothing here changes what goes on the wire.

One note on a claim in the audit that I could not confirm, because it changes the reasoning rather than the outcome: std::sync::OnceLock::get_or_init does not have a benign construct-and-discard race. It blocks concurrent initializers and runs the closure exactly once, so the "is construction side-effect free" question never arises. It is free anyway (AppStateProcessor::new is two Arc clones and an empty map; build_typed_ttl is a cache builder), but the guarantee is stronger than the audit assumed and there is a test for it.

Changes

  • group_cache -> std::sync::OnceLock<Arc<GroupCache>>. Confirmed by grep that no path resets it, cleanup_connection_state included. get_group_cache is no longer async and now returns &Arc<GroupCache>, so the group send path reads it with an atomic load and no Arc clone at all (it used to be forced to clone under the guard). memory_report uses .get(), so producing a report can no longer be the thing that builds the cache.
  • app_state_processor -> std::sync::OnceLock<Arc<AppStateProcessor>>. Same shape; lifecycle.rs calls clear_key_cache() on the processor and never replaces it. get_app_state_processor is pub(crate), so the signature change is internal.
  • noise_socket -> std::sync::Mutex. Rewritten per connection, so a OnceLock is out. All four critical sections are a clone or a store, and the non-Send guard now makes holding one across an .await a compile error on the send path, which every send_node goes through. The read loop already hoisted this lock out of the per-frame path and is_connected() already avoids probing it; the send path never got the same treatment.
  • pending_device_sync -> std::sync::Mutex, add/take_all/clear sync. They were async fn only because of the lock.
  • presence_subscriptions -> std::sync::Mutex, helpers sync. The N+1 re-acquisition in resubscribe_presence_subscriptions is kept deliberately: the per-JID re-check covers an unsubscribe that lands after the snapshot was taken, which is real behaviour and not waste. There is a test for exactly that interleaving, and I confirmed it fails if the re-check is removed.
  • chatstate_handlers -> std::sync::RwLock<Arc<[ChatStateHandler]>> with copy-on-write registration, guarded by an atomic count in the style of the existing node_waiters/node_waiter_count pair. Dispatch used to clone the whole Vec and build a ChatStateEvent unconditionally, even in the default case of zero handlers; now zero handlers means neither the lock nor the event build. The Event::ChatPresence bus dispatch above it is untouched.

Breaking: Client::register_chatstate_handler is no longer async. Migration: drop the .await (client.register_chatstate_handler(h)).

Breaking: the public Client::group_cache field changes type from Mutex<Option<Arc<GroupCache>>> to OnceLock<Arc<GroupCache>>. Migration: client.group_cache.lock().await.clone() becomes client.group_cache.get().cloned().

Cost

Lock acquisitions per send, counted with temporary atomic counters on the getters and a warm cached group read plus one outgoing stanza (GROUP_CACHE_ACQ=1 NOISE_SOCKET_ACQ=1):

  • 1 async-mutex acquisition + 1 Arc clone per group send eliminated outright (group_cache -> atomic load, and the getter returns a reference so callers no longer clone).
  • 1 async-mutex acquisition per outgoing stanza downgraded to an uncontended std::sync::Mutex acquisition (noise_socket; still one acquisition, just a cheaper one).

Timing is in-process and synthetic: a pinned (taskset -c 0-3) release-build harness, 400k iterations per case, baseline and patch in the same binary, 3 rounds, medians below. The baselines are the old shapes holding the same Arcs so the clone is paid identically on both sides, and the control is client.transport — an async_lock::Mutex<Option<Arc<_>>> this PR does not touch, i.e. literally what noise_socket used to be. I could not use divan here: both getters are pub(crate) and a benches/ target is a separate crate, so it cannot reach them. The harness is not part of this PR.

ns/op, aggregate over all tasks:

case 1 task 4 tasks 16 tasks
control: transport async_lock (unchanged) 48.6 48.9 48.3
baseline: group_cache async_lock 57.9 58.9 57.1
patch: group_cache OnceLock + Arc clone 20.5 119.6 123.7
patch: group_cache OnceLock, no clone (what the real path does) 9.8 33.2 35.6
baseline: noise_socket async_lock 57.8 57.8 57.0
patch: noise_socket std::sync::Mutex 27.4 226.0 243.0
isolate: bare Arc::clone, no lock at all 15.4 76.4 72.0

Uncontended, which is the realistic per-send case: 57.9 -> 9.8 ns for group_cache and 57.8 -> 27.4 ns for noise_socket.

The contended columns look like a regression and I am not going to hide them, but they are not measuring the lock. A bare Arc::clone of a shared Arc with no lock at all already costs 72-76 ns/op in the same loop, so most of the patched contended figure is refcount cache-line ping-pong that both shapes pay. The async baselines stay flat at ~57 ns precisely because the async mutex serializes the tight loop and keeps that cache line on one core — flatness there is an artifact of the tasks not running in parallel, not a win. The OnceLock arm that does not clone stays at 33-36 ns even at 16 tasks, below the async baseline. And the workload itself is pathological: four cores re-reading the same cell at ~10M ops/s/core with zero work in between, which no send path approaches.

So, honestly: this is single-digit-to-tens-of-nanoseconds per send in the uncontended case, and the microbenchmark cannot cleanly speak to the contended one. The argument I would actually defend the PR on is the structural one — one lock acquisition per group send gone entirely, and a guard that the compiler now refuses to let anyone hold across an .await on the send path.

Checked and not changed

  • noise_socket as a OnceLock. Dropped, as the scope anticipated: lifecycle.rs sets it on connect and clears it to None on cleanup, so it is genuinely rewritten per connection. It got the sync-mutex treatment instead.
  • The get_or_init "benign race". Not confirmed, see Summary. std::sync::OnceLock::get_or_init blocks concurrent initializers rather than letting two construct and one discard, so no construction is ever thrown away. concurrent_first_readers_agree_on_one_instance covers it.
  • No existing assertion was changed. The ~30 test call sites that lost a .await are call adaptation only; the full lib suite passes unmodified otherwise.

Validation

cargo fmt --all
cargo test -p whatsapp-rust --lib          # 1373 passed, 0 failed
cargo test --doc -p whatsapp-rust          # 1 passed
cargo clippy -p whatsapp-rust --lib --tests --all-features -- -D warnings

New tests: write-once cells return the same instance across calls and survive cleanup_connection_state; 16 concurrent first readers agree on one instance; chatstate dispatch with zero handlers builds no event (proved with a #[cfg(test)] build counter, not just "no handler ran") and with two handlers reaches both; and a gated-transport test that lands an unsubscribe after the resubscribe snapshot but before the loop reaches that JID, asserting it is not re-subscribed. That last one I mutation-checked: it fails (left: 2, right: 1) when the per-JID re-check is disabled.

Full matrix left to CI. E2E not run — no mock server available here.


Generated by Claude Code

Six pieces of client state sat behind an async lock whose critical section
never awaited, and most of them were written once and then only read.

group_cache and app_state_processor are lazy-init cells that nothing ever
resets: the only writes are the initial None and the getter's fill, and the
reconnect cleanup clears the processor's key cache in place rather than
replacing it. Both become std::sync::OnceLock, so the group send path reads
them with an atomic load and no Arc clone (the getters now hand back &Arc).

noise_socket is replaced per connection, so it stays a cell, but every one of
its four critical sections is a clone or a store. A std::sync::Mutex makes it
a compile error to hold that guard across an await on the send path, which is
what send_node has always relied on by convention.

pending_device_sync and presence_subscriptions were async only because of the
lock; their methods become sync. The per-JID re-check in the presence
resubscribe loop stays: it covers an unsubscribe that lands after the snapshot
was taken, and with a sync lock it is cheap.

chatstate_handlers becomes a copy-on-write Arc<[_]> behind a sync RwLock,
guarded by an atomic count the way node_waiters already is, so the default
(no handler registered) neither takes the lock nor builds the ChatStateEvent
that only a handler would read.

BREAKING: Client::register_chatstate_handler is no longer async; drop the
.await. Client::group_cache changes type from Mutex<Option<Arc<GroupCache>>>
to OnceLock<Arc<GroupCache>>.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved presence resynchronization so recently removed subscriptions are not restored unexpectedly.
    • Improved reconnect and device-sync handling for more reliable state recovery.
  • Performance

    • Reduced unnecessary waiting during connection, messaging, group-cache, and app-state operations.
    • Avoided creating unused chat-state events when no handlers are registered.
  • Tests

    • Added coverage for concurrent initialization, reconnect cleanup, presence changes during resynchronization, and chat-state dispatch behavior.

Walkthrough

The client replaces selected async mutexes with synchronous locks and OnceLock initialization. Chat-state dispatch uses immutable snapshots and an atomic count. Presence and pending device synchronization APIs become synchronous, with updated call sites and concurrency tests.

Changes

Client synchronization refactor

Layer / File(s) Summary
Client state initialization and accessors
src/client.rs, src/client/accessors.rs, src/client/adapters.rs, src/client/lifecycle.rs, src/client/tests.rs, src/client/voip.rs, src/handlers/call.rs, src/message/tests.rs, src/retry.rs, src/test_utils.rs, src/voip/facade.rs
Client state uses synchronous mutexes or write-once cells. Lifecycle paths and tests verify stable object identity across reads, cleanup, and concurrent initialization.
Chat-state handler snapshots
src/client.rs, src/client/lifecycle.rs, src/client/accessors.rs, src/client/messaging.rs, src/client/tests.rs
Handlers use copy-on-write snapshots and an atomic count. Dispatch skips event construction when no handler exists and builds one event for all handlers.
Synchronous presence tracking
src/features/presence.rs, src/client.rs, src/client/accessors.rs
Presence tracking uses poison-tolerant synchronous locking. Resubscription checks each JID immediately before sending. Tests cover unsubscribe during resubscription.
Pending device synchronization
src/pending_device_sync.rs, src/message/receive.rs, src/usync.rs, src/client/lifecycle.rs, src/client/device_registry.rs
PendingDeviceSync::add, take_all, and clear use synchronous locking. Production paths and tests call these methods synchronously.
Synchronous API call-site migration
src/client/app_state.rs, src/features/chat_actions.rs, src/features/groups.rs, src/client/messaging.rs, src/client/node_io.rs
App-state, group-cache, and noise-socket callers remove accessor waits while retaining asynchronous cache and transport operations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Presence
  participant Transport
  Client->>Presence: read tracked JIDs
  Presence-->>Client: return snapshot
  Client->>Presence: recheck JID
  Presence->>Transport: send resubscription
  Client->>Presence: remove unsubscribed JID
Loading

Possibly related PRs

Suggested labels: performance, breaking-change, api-design

Suggested reviewers: cubic-dev-ai, greptile-apps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: replacing selected async mutexes with write-once or synchronous state primitives.
Description check ✅ Passed The description directly explains the synchronization changes, affected APIs, performance rationale, tests, and validation results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/write-once-cells

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces several short async-lock critical sections with synchronous locks or write-once cells while preserving the connection and cache lifecycles. Major changes:

  • Moves group and app-state processor initialization into OnceLock.
  • Uses synchronous mutexes for the replaceable Noise socket, presence subscriptions, and pending device-sync set.
  • Adds copy-on-write chat-state handler registration with a zero-handler dispatch fast path.
  • Updates affected callers and adds concurrency and lifecycle regression tests.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
src/client.rs Changes shared-state field types and introduces the chat-state handler count used by the dispatch fast path.
src/client/accessors.rs Makes group-cache initialization synchronous and reads new synchronous state in memory reporting.
src/client/app_state.rs Converts the app-state processor to write-once initialization while retaining its existing backend and runtime dependencies.
src/client/lifecycle.rs Initializes the new lock types and preserves per-connection Noise-socket replacement and app-state key-cache cleanup.
src/client/messaging.rs Uses synchronous Noise-socket access and copy-on-write chat-state handler publication.
src/features/presence.rs Converts subscription tracking to synchronous locking while retaining per-JID checks around the awaited token lookup.
src/pending_device_sync.rs Replaces the async set lock with a synchronous mutex without changing add, drain, or clear semantics.
src/client/tests.rs Adds lifecycle and concurrency coverage for write-once cells and chat-state dispatch behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Caller[Client callers] --> Group[Group cache OnceLock]
  Caller --> AppState[App-state processor OnceLock]
  Caller --> Noise[Noise socket sync Mutex]
  Caller --> Presence[Presence subscriptions sync Mutex]
  Caller --> DeviceSync[Pending device sync Mutex]
  Register[Register chat-state handler] --> Snapshot[Copy-on-write handler snapshot]
  Snapshot --> Count[Atomic handler count]
  Count --> Dispatch{Count nonzero?}
  Dispatch -- Yes --> Snapshot
  Dispatch -- No --> Skip[Skip event construction]
Loading

Reviews (6): Last reviewed commit: "fix(presence): re-read tracking after th..." | Re-trigger Greptile

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 7, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 21 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/client/lifecycle.rs
Comment thread src/features/presence.rs
Comment thread src/client/tests.rs Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 9.99 MiB 9.99 MiB -4.03 KiB (-0.04%) 🔽
bin .text 8.01 MiB 8.00 MiB -4.25 KiB (-0.05%) 🔽
bin allocated (text+data+bss) 9.99 MiB 9.99 MiB -4.13 KiB (-0.04%) 🔽
llvm-lines wacore 515,449 515,449 0
llvm-lines wacore copies 16,831 16,831 0
llvm-lines whatsapp-rust lib 737,807 735,964 -1,843 (-0.25%) 🔽
llvm-lines whatsapp-rust lib copies 23,194 23,150 -44 (-0.19%) 🔽
deps crates (Cargo.lock) 462 462 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.83 MiB 1.82 MiB -6.08 KiB (-0.32%) 🔽
.text wacore 695.02 KiB 695.02 KiB 0
.text wacore_binary 88.60 KiB 88.60 KiB 0
.text wacore_libsignal 173.44 KiB 173.44 KiB 0
.text wacore_appstate 22.35 KiB 22.35 KiB 0
.text wacore_noise 20.94 KiB 20.94 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 515.62 KiB 515.62 KiB 0
.text whatsapp_rust_tokio_transport 40.49 KiB 40.49 KiB 0
.text whatsapp_rust_ureq_http_client 11.83 KiB 11.83 KiB 0
.text std 989.79 KiB 992.42 KiB +2.63 KiB (+0.27%) 🔺
.text other deps 1.90 MiB 1.90 MiB -900 B (-0.05%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.83 MiB 1.82 MiB -6.08 KiB (-0.32%)
std 989.79 KiB 992.42 KiB +2.63 KiB (+0.27%)

Baseline: 3da386926 (latest main run) · Head: 4404e8286 · Graphs

Sixteen spawned readers do not necessarily overlap: the first can finish
initializing the cell before the last is spawned, in which case the test
observes a warm read and never exercises the initializer path it exists for.
A start barrier holds every reader until all sixteen are ready.
@greptile-apps
greptile-apps Bot dismissed their stale review August 7, 2026 07:34

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 7, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/client/tests.rs Outdated
An async barrier releases sixteen tasks onto four worker threads, so at most
four are ever inside the initializer together and the rest arrive to a warm
cell. Both getters are synchronous now, so the readers can be OS threads
behind a blocking barrier instead, which puts all sixteen at the cell at once.
@greptile-apps
greptile-apps Bot dismissed their stale review August 7, 2026 07:42

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 7, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Pure lock-type swap (OnceLock, sync Mutex, RwLock) for six state fields whose critical sections are trivially sync; no behavior change, verified by tests and benchmarks, no operational or security tradeoff introduced.

Re-trigger cubic

@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@claude fix the conflicts

@claude

claude Bot commented Aug 7, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 12m 52s —— View job


Merge resolved locally — push blocked by workflow permissions

  • Merge origin/main (3da3869) into this branch, resolving every conflict
  • Verify tree matches origin/main + this branch's PR diff (434/-143, unchanged)
  • Grep for stale .await on signal_adapter(), sender_key_adapter(), noise_socket.lock(), pending_device_sync.*, get_group_cache() — none
  • ❌ Push — blocked: .github/workflows/claude.yml grants only contents: read, so git push returns 403 denied to github-actions[bot]

What the merge does

Origin/main's #1226 refactored signal_adapter / sender_key_adapter / signal_adapter_from to be sync and take Arc<PersistenceManager> instead of Arc<RwLock<Device>>. This branch made get_noise_socket sync, pending_device_sync sync, and get_group_cache sync. All of those coexist — nothing textually conflicts once you replay both sides. Overlapping files (adapters.rs, device_registry.rs, message/receive.rs, message/tests.rs, retry.rs, test_utils.rs, voip/facade.rs) were manually 3-way-merged; non-overlapping origin/main files were taken verbatim.

The commit (085b78e2 locally) sits on top of the existing e1a9ead0 merge commit as a single-parent commit rather than a real merge commit — I can't run git merge in this sandbox — but the tree is identical to what a clean git merge origin/main would produce.

To land it

Either grant the workflow contents: write (add to .github/workflows/claude.yml under permissions: — I can't edit workflow files myself), or reproduce locally:

git fetch origin main
git merge origin/main
# resolve the same overlapping files as above; ns-level, mechanical
git push

I cannot modify workflow files due to GitHub App permissions — see the FAQ.

Also: I could not run cargo check in this sandbox, so CI is the authoritative verification.
· Branch: perf/write-once-cells

@greptile-apps
greptile-apps Bot dismissed their stale review August 7, 2026 15:25

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/presence.rs (1)

152-158: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Re-check tracking immediately before send_node.

build_subscription_node() awaits lookup_tc_token_for_jid(). An unsubscribe() can send its unsubscribe stanza and remove jid while that await is pending. This method can then send a subscribe stanza after the unsubscribe and leave the remote subscription active while local tracking is inactive.

Proposed fix
         let node = self.build_subscription_node(jid).await;
+        if !self.client.is_presence_subscription_tracked(jid) {
+            return Ok(());
+        }
         self.client.send_node(node).await?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/presence.rs` around lines 152 - 158, Update
re_subscribe_when_active to re-check is_presence_subscription_tracked(jid) after
build_subscription_node(jid).await and immediately before send_node; return
Ok(()) when tracking was removed during the await, otherwise preserve the
existing send behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/presence.rs`:
- Around line 539-540: Update the JID literals in the relevant presence test to
use valid fictitious NANP numbers with a real non-555 NPA and subscriber numbers
in the 555-0100 through 555-0199 range, such as 12025550111 and 12025550122;
keep the existing parsing and assertions unchanged.

In `@src/usync.rs`:
- Line 516: Update the call to PendingDeviceSync::add in the surrounding sync
logic to explicitly bind its bool result to _, preserving concurrent re-enqueue
behavior while satisfying unused_must_use and workspace Clippy with -D warnings.

---

Outside diff comments:
In `@src/features/presence.rs`:
- Around line 152-158: Update re_subscribe_when_active to re-check
is_presence_subscription_tracked(jid) after build_subscription_node(jid).await
and immediately before send_node; return Ok(()) when tracking was removed during
the await, otherwise preserve the existing send behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 38a3384e-d791-4711-b58c-93d4e553e321

📥 Commits

Reviewing files that changed from the base of the PR and between abf1b68 and e1a9ead.

📒 Files selected for processing (21)
  • src/client.rs
  • src/client/accessors.rs
  • src/client/adapters.rs
  • src/client/app_state.rs
  • src/client/device_registry.rs
  • src/client/lifecycle.rs
  • src/client/messaging.rs
  • src/client/node_io.rs
  • src/client/tests.rs
  • src/client/voip.rs
  • src/features/chat_actions.rs
  • src/features/groups.rs
  • src/features/presence.rs
  • src/handlers/call.rs
  • src/message/receive.rs
  • src/message/tests.rs
  • src/pending_device_sync.rs
  • src/retry.rs
  • src/test_utils.rs
  • src/usync.rs
  • src/voip/facade.rs
💤 Files with no reviewable changes (1)
  • src/client/node_io.rs

Comment thread src/features/presence.rs Outdated
Comment thread src/usync.rs
`build_subscription_node` awaits a tctoken lookup. An `unsubscribe` landing in
that window has already sent its own stanza, so subscribing afterwards leaves
the peer subscribed while we no longer track it. Re-read before the send.

This narrows the window rather than closing it — `send_node` awaits too — but
the lookup is the wide half and the re-read is an uncontended lock.

Also moves the test JIDs onto the fictitious NANP form used elsewhere in the
repo (real NPA, 555-01xx subscriber) instead of a 555 area code.
@greptile-apps
greptile-apps Bot dismissed their stale review August 7, 2026 15:47

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/presence.rs`:
- Around line 158-166: Serialize unsubscribe and re-subscribe operations in the
presence flow around re_subscribe_when_active and the unsubscribe handler,
holding the operation ordering through send completion so an unsubscribe cannot
overtake a checked re-subscribe. Remove reliance on the final synchronous
tracking check before send_node; use the serialized mechanism to make the intent
and transport send linearizable. Add a regression test that pauses node
construction or send handoff after the final check, completes unsubscribe, and
verifies no stale subscribe reaches the transport.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 81724c0a-ad48-4d16-ba4a-fb28753f9363

📥 Commits

Reviewing files that changed from the base of the PR and between 6318f16 and e003ab5.

📒 Files selected for processing (1)
  • src/features/presence.rs

Comment thread src/features/presence.rs
@jlucaso1
jlucaso1 merged commit 534054f into main Aug 7, 2026
42 checks passed
@jlucaso1
jlucaso1 deleted the perf/write-once-cells branch August 7, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants