Skip to content

perf: reduce duplication and unnecessary allocations - #499

Merged
jlucaso1 merged 5 commits into
mainfrom
perf/dry-and-reduce-allocations
Apr 7, 2026
Merged

perf: reduce duplication and unnecessary allocations#499
jlucaso1 merged 5 commits into
mainfrom
perf/dry-and-reduce-allocations

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

29 files changed, net -46 lines

DRY

  • Unify NodeWaiter/SentNodeWaiter into single type with shared resolve_waiters() helper
  • Extract build_revoke_message() — 3 identical constructions → 1 function
  • Migrate to SenderKeyName::from_jid() — 4 call sites eliminated manual .to_string() pairs
  • Reuse existing notification_timestamp() — 2 sites had copy-pasted its body
  • Extract mapped_server() — shared by to_signal_address_string(), write_protocol_address_to(), and cmp_for_lock_order()

Allocation reductions

  • Zero-alloc lock key sorting — sort Vec<Jid> by fields directly instead of allocating Vec<ProtocolAddress> (2 Strings per device). Callers reuse a single String buffer for lock lookups. Benchmarked with iai-callgrind: 1.3-2.8x faster, 49-95% fewer RAM hits
  • From<&Jid> for NodeValue — new impl enabling ~36 .clone() removals across IQ specs, stanzas, receipts, retry, features, and request building
  • Pass Jids directly to NodeBuilder::attr() as NodeValue::Jid — compact binary JID_PAIR/AD_JID encoding instead of stringifying (verified against WA Web's Wap.js)
  • NodeValue::to_jid() instead of to_string().parse::<Jid>() in group send participant dedup
  • HashMap<&Jid, Jid> for device encryption map — borrows keys from input slice
  • ProtocolAddress::as_str() instead of .to_string() for session lock keys
  • as_str() over to_string() for presence timestamp parsing
  • Move message_id instead of .clone() in revoke_message() (value unused after)

Correctness fixes

  • dedup_by(cmp_for_lock_order) instead of dedup() — JIDs that differ only in agent/integrator but map to the same lock key are now correctly deduplicated (prevents double-locking the same mutex)
  • Session lock for identity change deletiondelete_session + delete_identity now held under per-sender session lock (mirrors Signal::delete_sessions() pattern, prevents race with concurrent encrypt/decrypt)
  • Session lock for reg-ID mismatch deletion — same race condition fixed in retry path

Logging

  • flush_signal_cache_logged accepts optional message ID for per-message log correlation without allocation (borrows existing &info.id)

Verified against WhatsApp Web

  • JID binary encoding uses JID_PAIR/AD_JID tokens (matches Wap.js:159-180)
  • Revoke message key structure matches RevokeMessageUtils.js:78-84
  • Notification timestamp extraction matches attrInt("t") pattern
  • Session deletion follows deleteRemoteInfo() dual-delete pattern from SessionApi.js

Test plan

  • cargo fmt --all
  • cargo clippy --all --tests — zero warnings
  • cargo test --workspace --exclude e2e-tests — 1119 tests pass
  • iai-callgrind benchmarks validate lock key optimization gains

Summary by CodeRabbit

  • Refactor
    • Consolidated internal waiter cleanup logic and improved session lock handling across multiple components.
    • Optimized memory allocation patterns by removing unnecessary string conversions and data structure clones throughout the codebase.
    • Simplified attribute construction in protocol message builders.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors string handling and memory allocation patterns across the codebase by consolidating waiter-resolution logic, passing borrowed Jid references instead of clones to node builders, updating function signatures with structured parameters, and introducing helpers for protocol-address serialization and lock ordering.

Changes

Cohort / File(s) Summary
Waiter Resolution
src/client.rs
Consolidated waiter cleanup logic into a shared resolve_waiters(...) helper; refactored resolve_node_waiters and resolve_sent_node_waiters to delegate to it; replaced SentNodeWaiter type with NodeWaiter; extended flush_signal_cache_logged signature to accept optional id parameter; removed unnecessary call_from cloning in reject_call.
Signal & Session Operations
src/features/signal.rs, src/handlers/notification.rs, src/handlers/presence.rs, src/retry.rs
Updated session-lock acquisition to pass &str directly instead of allocating owned strings; replaced SenderKeyName::new(...) calls with SenderKeyName::from_jid(...) to avoid clones; added per-session locking during cache deletion in handle_identity_change; consolidated timestamp parsing into notification_timestamp helper; updated flush_signal_cache_logged call sites with additional None parameter; simplified parse-from-attribute patterns.
Message & Receipt Handling
src/message.rs, src/send.rs, src/receipt.rs
Updated flush_signal_cache_logged calls to pass structured context and optional ID instead of formatted strings; added build_revoke_message helper to centralize revoke-message construction; refactored session-lock derivation to use Jid types instead of strings; eliminated Jid clones in receipt-attribute setting.
IQ & Request Building (wacore)
wacore/src/iq/{dirty,usync,business,devices,groups,privacy,tctoken}.rs, wacore/src/request.rs, wacore/binary/src/node.rs
Removed unnecessary Jid clones when setting node attributes across all IQ specs (business, devices, groups, privacy, tokens); changed attribute values from jid.clone() to borrowed references; added impl From<&Jid> for NodeValue to enable direct use of &Jid in node builders; updated attribute construction to avoid to_string() allocations on Jid conversions.
Protocol Address & Lock Helpers (wacore)
wacore/src/types/jid.rs, wacore/src/send.rs, wacore/src/usync.rs, wacore/src/media_retry.rs, wacore/src/prekeys.rs
Added write_protocol_address_to(...) for buffer-reusable protocol-address serialization; added cmp_for_lock_order(...) for consistent Jid ordering; added mapped_server(...) helper for server mapping; updated to_signal_address_string() to use centralized mapping; removed intermediate to_string() allocations in usync/media-retry attribute setting; changed jid_to_encryption_jid map key type from HashMap<Jid, Jid> to HashMap<&Jid, Jid>.
Feature Modules & Device Registry
src/features/{chatstate,newsletter,presence}.rs, src/client/device_registry.rs
Removed Jid clones in stanza builders (to attribute); updated delete_sessions_for_devices flush call to pass None parameter.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Ari4ka

🐰 Clones are shed like fur in spring,
Borrowed references now take wing,
Helpers consolidate the fray,
Less allocation — hip, hooray!
The code flows leaner, brighter, cleaner.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'perf: reduce duplication and unnecessary allocations' accurately summarizes the main changes: it introduces DRY refactoring (consolidating waiter types, extracting helpers, using shared functions) and performs allocation reductions (zero-alloc lock key sorting, removing clones via From<&Jid> for NodeValue, and optimizing string conversions). The title is concise, clear, and directly reflects the primary objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/dry-and-reduce-allocations

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 and usage tips.

@jlucaso1

jlucaso1 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 7, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 20s —— View job


Code Review: perf: reduce duplication and unnecessary allocations

  • Read PR diff and changed files
  • Review correctness and safety
  • Post review feedback

Clean PR — the deduplication and allocation reductions are all well-motivated and correct. A few observations:

Looks Good

  • resolve_waiters() extraction (src/client.rs): Clean factoring. The free function taking &Mutex + &AtomicUsize is the right call vs. adding a method — keeps the two waiter lists clearly independent. Removing the identical SentNodeWaiter struct is a nice cleanup.

  • HashMap<&Jid, Jid> borrow (wacore/src/send.rs:350): Sound — devices: &[Jid] outlives the map. Eliminates 3 .clone() calls per device in the hot path. Good change.

  • From<&Jid> for NodeValue (wacore/binary/src/node.rs): Clean impl, avoids .to_string() at 8+ call sites. The jid.clone() inside is necessary since NodeValue::Jid owns its data.

  • build_revoke_message() extraction (src/send.rs:183-201): Straightforward — 3 identical constructions consolidated with no behavior change.

  • notification_timestamp() reuse (src/handlers/notification.rs): Good catch on the duplicated helper body.

  • as_str() over to_string() for session lock keys and presence parsing: All correct zero-copy replacements.

Minor Nit

  • build_session_lock_keys sort/dedup (src/send.rs:1496-1497): Since ProtocolAddress already implements Ord and Eq via self.display (the same field as_str() returns), you can simplify:

    keys.sort_unstable();
    keys.dedup();

    instead of the manual sort_unstable_by(|a, b| a.as_str().cmp(b.as_str())) / dedup_by(|a, b| a.as_str() == b.as_str()). Same semantics, less noise. Fix this →

Overall

LGTM — net -40 lines, all changes are mechanical deduplication or zero-copy improvements with no semantic changes. The HashMap<&Jid, Jid> borrow optimization is particularly nice for the group send path.


@github-actions

github-actions Bot commented Apr 7, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchperf/dry-and-reduce-allocations
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()Instructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
47.03 x 1e3
(+7.48%)Baseline: 43.76 x 1e3
45.95 x 1e3
(102.36%)

Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,199.00
(-2.62%)Baseline: 6,366.09
6,684.40
(92.74%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
523,708.00
(-23.83%)Baseline: 687,530.33
721,906.85
(72.55%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,910.00
(-4.61%)Baseline: 21,919.71
23,015.70
(90.85%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,703.00
(-10.99%)Baseline: 110,888.75
116,433.19
(84.77%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,731.00
(-7.95%)Baseline: 107,253.12
112,615.78
(87.67%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
533,017.00
(-0.08%)Baseline: 533,419.93
560,090.92
(95.17%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,955.00
(-3.38%)Baseline: 16,512.47
17,338.09
(92.02%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,813,622.00
(-6.00%)Baseline: 15,759,440.10
16,547,412.11
(89.52%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,631.00
(-17.24%)Baseline: 143,348.91
150,516.36
(78.82%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,447.00
(-0.07%)Baseline: 534,842.50
561,584.62
(95.17%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
18,004.00
(-3.01%)Baseline: 18,563.35
19,491.51
(92.37%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,200,726.00
(-17.95%)Baseline: 34,371,848.42
36,090,440.84
(78.14%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,456.00
(-0.08%)Baseline: 533,858.93
560,551.87
(95.17%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,928.00
(-5.78%)Baseline: 16,905.38
17,750.65
(89.73%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,815,122.00
(-6.00%)Baseline: 15,760,621.21
16,548,652.27
(89.52%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
108,446.00
(-8.76%)Baseline: 118,851.61
124,794.19
(86.90%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,803.00
(-7.94%)Baseline: 107,325.12
112,691.38
(87.68%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,558.00
(-3.64%)Baseline: 95,012.91
99,763.56
(91.77%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,431.00
(-1.89%)Baseline: 7,574.11
7,952.81
(93.44%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,589.00
(-0.86%)Baseline: 92,381.62
97,000.71
(94.42%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,454.00
(+0.98%)Baseline: 7,381.59
7,750.67
(96.17%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
107,134.00
(-0.95%)Baseline: 108,156.24
113,564.05
(94.34%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,966.00
(+0.81%)Baseline: 8,893.59
9,338.27
(96.01%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-5.63%)Baseline: 44,494.67
46,719.41
(89.87%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,716.00
(-1.51%)Baseline: 2,757.75
2,895.63
(93.80%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,090.00
(-0.01%)Baseline: 556,138.87
583,945.82
(95.23%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
773.00
(+0.10%)Baseline: 772.22
810.83
(95.33%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,396,594.00
(-1.07%)Baseline: 27,693,951.91
29,078,649.51
(94.22%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,512,366.00
(-0.61%)Baseline: 5,546,341.06
5,823,658.12
(94.65%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
162,318.00
(-8.31%)Baseline: 177,026.91
185,878.25
(87.32%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
163,047.00
(-8.29%)Baseline: 177,785.08
186,674.33
(87.34%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,377,042.00
(+0.56%)Baseline: 17,280,769.35
18,144,807.82
(95.77%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
298,493.00
(+0.49%)Baseline: 297,043.75
311,895.94
(95.70%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,659,075.00
(+0.51%)Baseline: 12,595,388.25
13,225,157.66
(95.72%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
713,164.00
(-0.57%)Baseline: 717,236.65
753,098.48
(94.70%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
🚨 view alert (🔔)
47,033.00
(+7.48%)Baseline: 43,760.83
45,948.87
(102.36%)

libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
14,247,117.00
(-8.42%)Baseline: 15,556,300.15
16,334,115.15
(87.22%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,116,228.00
(-6.36%)Baseline: 5,463,875.64
5,737,069.42
(89.18%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
298,844.00
(-59.64%)Baseline: 740,475.78
777,499.57
(38.44%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,495.00
(+0.15%)Baseline: 2,826,179.42
2,967,488.39
(95.38%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,467,011.00
(-0.05%)Baseline: 3,468,677.63
3,642,111.51
(95.19%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
125,419,853.00
(+0.04%)Baseline: 125,370,639.23
131,639,171.19
(95.28%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,851.00
(+0.07%)Baseline: 11,842.26
12,434.38
(95.31%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,879.00
(+0.84%)Baseline: 3,846.71
4,039.05
(96.04%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
77,193.00
(-11.88%)Baseline: 87,598.29
91,978.20
(83.93%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
69,103.00
(-13.21%)Baseline: 79,624.18
83,605.39
(82.65%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
43,398.00
(-14.58%)Baseline: 50,803.05
53,343.20
(81.36%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,939.00
(+2.35%)Baseline: 5,802.88
6,093.02
(97.47%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,214.00
(+3.19%)Baseline: 2,145.54
2,252.81
(98.28%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
19,365.00
(-11.62%)Baseline: 21,910.20
23,005.71
(84.17%)
🐰 View full continuous benchmarking report in Bencher

DRY improvements:
- Unify NodeWaiter/SentNodeWaiter into single type with shared resolve_waiters()
- Extract build_revoke_message() helper (3 identical constructions → 1)
- Replace SenderKeyName::new(x.to_string(), y.to_string()) with from_jid() (4 sites)
- Use existing notification_timestamp() helper instead of copy-pasted body (2 sites)

Allocation reductions:
- Use ProtocolAddress::as_str() instead of .to_string() for session lock keys
- Return Vec<ProtocolAddress> from build_session_lock_keys() instead of Vec<String>
- Use NodeValue::to_jid() instead of to_string().parse::<Jid>() in group send path
- Borrow Jid keys in device encryption HashMap (HashMap<&Jid, Jid>)
- Add From<&Jid> for NodeValue and remove ~36 unnecessary .clone() calls on Jid
  values passed to NodeBuilder::attr() across 25 files
- Pass Jids directly to NodeBuilder::attr() as NodeValue::Jid (compact encoding)
- Use as_str() over to_string() for presence timestamp parsing
- Use static string for flush_signal_cache_logged context
- Simplify build_session_lock_keys sort/dedup (ProtocolAddress implements Ord)
@jlucaso1
jlucaso1 force-pushed the perf/dry-and-reduce-allocations branch from dedb8ba to 6d88e53 Compare April 7, 2026 13:08

@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

Caution

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

⚠️ Outside diff range comments (1)
wacore/src/send.rs (1)

392-415: 🧹 Nitpick | 🔵 Trivial

Only store rewritten encryption JIDs in jid_to_encryption_jid.

Line 392 and the no-rewrite branch below still clone and insert identity mappings, but both later consumers already fall back to device_jid when the map misses. Skipping those inserts keeps the common direct-session path actually zero-copy.

♻️ Suggested change
         if stores
             .session_store
             .load_session(&signal_address)
             .await?
             .is_some()
         {
             // Session exists under direct address, use it
-            jid_to_encryption_jid.insert(device_jid, device_jid.clone());
             continue;
         }

         // No session found - need to fetch prekeys and create session.
         // Keep device_jid for prekey fetch (server returns bundles keyed by this),
-        // but normalize to LID for the actual session creation.
-        let encryption_jid = if device_jid.is_pn() {
-            if let Some(lid_user) = resolver.get_lid_for_phone(&device_jid.user).await {
-                let lid_jid = Jid::lid_device(lid_user, device_jid.device);
-                log::debug!(
-                    "Will create LID session {} for PN {} (no existing session)",
-                    lid_jid,
-                    device_jid
-                );
-                lid_jid
-            } else {
-                device_jid.clone()
-            }
-        } else {
-            device_jid.clone()
-        };
-        jid_to_encryption_jid.insert(device_jid, encryption_jid);
+        // but normalize to LID for the actual session creation when needed.
+        if device_jid.is_pn()
+            && let Some(lid_user) = resolver.get_lid_for_phone(&device_jid.user).await
+        {
+            let lid_jid = Jid::lid_device(lid_user, device_jid.device);
+            log::debug!(
+                "Will create LID session {} for PN {} (no existing session)",
+                lid_jid,
+                device_jid
+            );
+            jid_to_encryption_jid.insert(device_jid, lid_jid);
+        }
         // Use original device_jid for prekey fetch (HashMap key match)
         jids_needing_prekeys.push(device_jid.clone());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/send.rs` around lines 392 - 415, The code currently inserts
identity mappings into jid_to_encryption_jid for device_jid clones even when no
rewrite occurs, wasting copies; change the logic in the block that computes
encryption_jid (which uses resolver.get_lid_for_phone and Jid::lid_device) to
only call jid_to_encryption_jid.insert(device_jid, encryption_jid) when
encryption_jid != device_jid (i.e., a LID rewrite was produced); leave the
original device_jid available and used for prekey fetch and do not insert the
identity mapping so the common direct-session path stays zero-copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/handlers/notification.rs`:
- Around line 894-895: The helper notification_timestamp currently casts a u64
to i64 with `as`, causing wraparound for values > i64::MAX; change
notification_timestamp to perform a checked conversion (use i64::try_from on the
optional_u64("t") value), then build the chrono::DateTime only if the conversion
succeeds, otherwise fall back to chrono::Utc::now(); update the function body
used by callers (notification_timestamp) so it chains optional_u64("t") ->
i64::try_from(...).ok() -> DateTime creation and unwraps with Utc::now() on
failure.

---

Outside diff comments:
In `@wacore/src/send.rs`:
- Around line 392-415: The code currently inserts identity mappings into
jid_to_encryption_jid for device_jid clones even when no rewrite occurs, wasting
copies; change the logic in the block that computes encryption_jid (which uses
resolver.get_lid_for_phone and Jid::lid_device) to only call
jid_to_encryption_jid.insert(device_jid, encryption_jid) when encryption_jid !=
device_jid (i.e., a LID rewrite was produced); leave the original device_jid
available and used for prekey fetch and do not insert the identity mapping so
the common direct-session path stays zero-copy.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b38e5fb-c4a3-4000-97a2-4b07f1285bd3

📥 Commits

Reviewing files that changed from the base of the PR and between 0a942ef and dedb8ba.

📒 Files selected for processing (13)
  • src/client.rs
  • src/features/signal.rs
  • src/handlers/notification.rs
  • src/handlers/presence.rs
  • src/message.rs
  • src/retry.rs
  • src/send.rs
  • wacore/binary/src/node.rs
  • wacore/src/iq/dirty.rs
  • wacore/src/iq/usync.rs
  • wacore/src/media_retry.rs
  • wacore/src/send.rs
  • wacore/src/usync.rs

Comment thread src/handlers/notification.rs

@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 the current code and only fix it if needed.

Inline comments:
In `@src/message.rs`:
- Line 581: The call to self.flush_signal_cache_logged("message dispatch").await
loses per-message correlation in logs; modify the flush_signal_cache_logged call
to accept and forward the message ID (or reference to the message's correlation
id) so logs include that context without allocating (e.g., add a parameter like
message_id: &str or &MessageId and pass the existing id from the current message
before awaiting). Update the function signature of flush_signal_cache_logged and
its callers (and log statements inside it) to accept this id reference and use
it in error/debug messages while preserving zero-allocation usage.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f0779057-4009-48ff-a29a-e3041a4937be

📥 Commits

Reviewing files that changed from the base of the PR and between dedb8ba and 6d88e53.

📒 Files selected for processing (25)
  • src/client.rs
  • src/features/chatstate.rs
  • src/features/newsletter.rs
  • src/features/presence.rs
  • src/features/signal.rs
  • src/handlers/notification.rs
  • src/handlers/presence.rs
  • src/message.rs
  • src/receipt.rs
  • src/retry.rs
  • src/send.rs
  • wacore/binary/src/node.rs
  • wacore/src/iq/business.rs
  • wacore/src/iq/devices.rs
  • wacore/src/iq/dirty.rs
  • wacore/src/iq/groups.rs
  • wacore/src/iq/privacy.rs
  • wacore/src/iq/tctoken.rs
  • wacore/src/iq/usync.rs
  • wacore/src/media_retry.rs
  • wacore/src/pair.rs
  • wacore/src/prekeys.rs
  • wacore/src/request.rs
  • wacore/src/send.rs
  • wacore/src/usync.rs

Comment thread src/message.rs Outdated
// Flush cached Signal state to DB (matches WA Web's flushBufferToDiskIfNotMemOnlyMode)
self.flush_signal_cache_logged(&format!("message {}", info.id))
.await;
self.flush_signal_cache_logged("message dispatch").await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Preserve per-message flush context without reintroducing allocations.

Line 581 drops message-level correlation in error logs. You can keep zero-allocation behavior and retain debuggability by passing the message ID directly.

Suggested tweak
-        self.flush_signal_cache_logged("message dispatch").await;
+        self.flush_signal_cache_logged(info.id.as_str()).await;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.flush_signal_cache_logged("message dispatch").await;
self.flush_signal_cache_logged(info.id.as_str()).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/message.rs` at line 581, The call to
self.flush_signal_cache_logged("message dispatch").await loses per-message
correlation in logs; modify the flush_signal_cache_logged call to accept and
forward the message ID (or reference to the message's correlation id) so logs
include that context without allocating (e.g., add a parameter like message_id:
&str or &MessageId and pass the existing id from the current message before
awaiting). Update the function signature of flush_signal_cache_logged and its
callers (and log statements inside it) to accept this id reference and use it in
error/debug messages while preserving zero-allocation usage.

Lock key optimization (benchmarked: 1.3-2.8x faster, 49-95% fewer RAM hits):
- Sort Vec<Jid> by fields directly instead of allocating Vec<ProtocolAddress>
- Add write_protocol_address_to() and cmp_for_lock_order() to wacore JidExt
- Callers reuse a single String buffer for lock lookups (1 alloc vs 2N)
- DRY: extracted mapped_server() shared by both new helpers and existing
  to_signal_address_string()

flush_signal_cache_logged:
- Accept optional message ID parameter for per-message log correlation
- Zero-allocation: passes &str reference from existing MessageInfo.id
- All other callers pass None (no behavior change)

Also: add iai-callgrind benchmark for lock key approaches

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 101c23d7ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/send.rs Outdated
Comment on lines 1499 to 1500
keys.sort_unstable_by(wacore::types::jid::cmp_for_lock_order);
keys.dedup();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Deduplicate by lock key string before acquiring session locks

build_session_lock_keys() now sorts with cmp_for_lock_order and then calls dedup() on Vec<Jid>, but the lock key used later is produced by write_protocol_address_to() (which drops distinctions like s.whatsapp.net vs c.us, and agent/integrator fields). That means two different Jid values can survive dedup() while still mapping to the same session-lock key, so the send path can push the same mutex twice and then deadlock when locking guards sequentially (second lock().await waits on the first guard held in the same task).

Useful? React with 👍 / 👎.

@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

Caution

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

⚠️ Outside diff range comments (2)
src/retry.rs (1)

225-237: ⚠️ Potential issue | 🟠 Major

Take the session lock around the retry invalidation path.

This registration-ID mismatch branch reads and deletes the cached session without session_lock_for(). A concurrent encrypt/decrypt for the same sender can race this teardown and recreate the stale session you're trying to discard.

🔒 Minimal fix shape
                     let signal_address = resolved_jid.to_protocol_address();
+                    let session_lock = self.session_lock_for(signal_address.as_str()).await;
+                    let _session_guard = session_lock.lock().await;
                     let device_store = self.persistence_manager.get_device_arc().await;
                     let device_guard = device_store.read().await;

As per coding guidelines: Use session_locks to serialize per-sender Signal encrypt/decrypt operations and message_enqueue_locks to serialize per-chat incoming message processing

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/retry.rs` around lines 225 - 237, The reg-ID mismatch branch reads and
deletes the cached session without holding the per-sender lock; acquire the
session lock via session_locks.session_lock_for(&signal_address).await (same
lock used for encrypt/decrypt) before reading session.remote_registration_id()
and performing self.signal_cache.delete_session(...) and
self.flush_signal_cache_logged(...).await, hold that lock for the whole
invalidation path so concurrent encrypt/decrypt cannot recreate the stale
session; release the lock after flush completes. Ensure you use the existing
session_locks construct (not message_enqueue_locks) and keep references to
signal_address, stored_reg_id and received_reg_id when logging.
src/handlers/notification.rs (1)

373-389: ⚠️ Potential issue | 🟠 Major

Serialize identity-change teardown with session_lock_for().

This branch still deletes the pairwise session and identity outside the per-sender mutex. A concurrent encrypt/decrypt on the same address can race this invalidation and repopulate stale state after the identity-change cleanup. Please mirror Signal::delete_sessions() here.

🔒 Minimal fix shape
         let resolved = client.resolve_encryption_jid(&from_jid).await;
         let addr = resolved.to_protocol_address();
-        client.signal_cache.delete_session(&addr).await;
-        client.signal_cache.delete_identity(&addr).await;
+        {
+            let session_lock = client.session_lock_for(addr.as_str()).await;
+            let _session_guard = session_lock.lock().await;
+            client.signal_cache.delete_session(&addr).await;
+            client.signal_cache.delete_identity(&addr).await;
+        }

As per coding guidelines: Use session_locks to serialize per-sender Signal encrypt/decrypt operations and message_enqueue_locks to serialize per-chat incoming message processing

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 373 - 389, The delete operations
for pairwise session, identity and per-sender sender-keys must be performed
under the per-sender mutex—wrap the teardown in session_lock_for() to serialize
with concurrent encrypt/decrypt. Concretely: after resolving the sender addr via
resolve_encryption_jid(&from_jid) acquire the session lock (use
session_lock_for(&addr).await) and inside that lock call
signal_cache.delete_session(&addr).await and
signal_cache.delete_identity(&addr).await; similarly, for each own_jid compute
the sender-key address (own_jid.to_protocol_address()) and acquire
session_lock_for(&that_addr).await before calling
signal_cache.delete_sender_key(sk_name.cache_key()).await (mirror the locking
pattern used in Signal::delete_sessions()). Keep
client.flush_signal_cache_logged("identity change", None).await outside or after
releasing the per-sender locks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/send.rs`:
- Line 733: The call to build_revoke_message unnecessarily clones message_id;
since message_id is unused afterwards, remove the allocation by passing
message_id by value (i.e., replace message_id.clone() with message_id) when
constructing revoke_message in the build_revoke_message call, ensuring the
function signature of build_revoke_message accepts ownership of message_id and
updating any calling context if needed.

---

Outside diff comments:
In `@src/handlers/notification.rs`:
- Around line 373-389: The delete operations for pairwise session, identity and
per-sender sender-keys must be performed under the per-sender mutex—wrap the
teardown in session_lock_for() to serialize with concurrent encrypt/decrypt.
Concretely: after resolving the sender addr via
resolve_encryption_jid(&from_jid) acquire the session lock (use
session_lock_for(&addr).await) and inside that lock call
signal_cache.delete_session(&addr).await and
signal_cache.delete_identity(&addr).await; similarly, for each own_jid compute
the sender-key address (own_jid.to_protocol_address()) and acquire
session_lock_for(&that_addr).await before calling
signal_cache.delete_sender_key(sk_name.cache_key()).await (mirror the locking
pattern used in Signal::delete_sessions()). Keep
client.flush_signal_cache_logged("identity change", None).await outside or after
releasing the per-sender locks.

In `@src/retry.rs`:
- Around line 225-237: The reg-ID mismatch branch reads and deletes the cached
session without holding the per-sender lock; acquire the session lock via
session_locks.session_lock_for(&signal_address).await (same lock used for
encrypt/decrypt) before reading session.remote_registration_id() and performing
self.signal_cache.delete_session(...) and
self.flush_signal_cache_logged(...).await, hold that lock for the whole
invalidation path so concurrent encrypt/decrypt cannot recreate the stale
session; release the lock after flush completes. Ensure you use the existing
session_locks construct (not message_enqueue_locks) and keep references to
signal_address, stored_reg_id and received_reg_id when logging.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c8a4815-31ed-4564-9609-5f270a98a17e

📥 Commits

Reviewing files that changed from the base of the PR and between 6d88e53 and 101c23d.

📒 Files selected for processing (10)
  • src/client.rs
  • src/client/device_registry.rs
  • src/features/signal.rs
  • src/handlers/notification.rs
  • src/message.rs
  • src/retry.rs
  • src/send.rs
  • wacore/Cargo.toml
  • wacore/benches/lock_key_benchmark.rs
  • wacore/src/types/jid.rs

Comment thread src/send.rs Outdated
@jlucaso1

jlucaso1 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 7, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 2m 7s —— View job


Code Review: perf: reduce duplication and unnecessary allocations

  • Read PR diff and changed files
  • Review core changes (client.rs, send.rs, jid.rs)
  • Review allocation optimizations across codebase
  • Evaluate Codex deadlock concern about dedup
  • Post review feedback

Clean PR overall — the DRY extractions are well-motivated and the borrow optimizations are correct. Two issues worth flagging:

Bug: dedup() uses Jid::PartialEq, not cmp_for_lock_order — potential deadlock

build_session_lock_keys (src/send.rs:1497) sorts with cmp_for_lock_order but deduplicates with keys.dedup(), which uses Jid's derived PartialEq. These have different equivalence classes:

  • cmp_for_lock_order compares (mapped_server, user, device) — maps s.whatsapp.netc.us
  • Jid::PartialEq compares (server, user, device, agent, integrator) — raw field equality

Two Jids that differ only in server (s.whatsapp.net vs c.us) or agent/integrator would survive dedup() but produce identical lock keys via write_protocol_address_to → same mutex acquired twice in sequence → deadlock.

The benchmark file (wacore/benches/lock_key_benchmark.rs:85) has the same bug.

In practice this may be unreachable today since resolve_encryption_jid always returns s.whatsapp.net (never c.us) for phone JIDs, but it's a correctness gap that's easy to fix:

keys.sort_unstable_by(cmp_for_lock_order);
keys.dedup_by(|a, b| cmp_for_lock_order(a, b).is_eq());

Fix this →

Nit: notification_timestampu64 as i64 wraps silently

(src/handlers/notification.rs:1019) The t as i64 cast wraps values > i64::MAX to negative, producing pre-1970 timestamps instead of falling back to Utc::now(). Practically unlikely from WhatsApp servers, but a checked conversion is trivial:

fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
    node.attrs()
        .optional_u64("t")
        .and_then(|t| i64::try_from(t).ok())
        .and_then(|t| chrono::DateTime::from_timestamp(t, 0))
        .unwrap_or_else(chrono::Utc::now)
}

Note: this was pre-existing before this PR (the PR just reuses the helper at two more call sites), so not a blocker.

Looks Good

  • resolve_waiters() extraction (src/client.rs:94): Clean free-function taking &Mutex + &AtomicUsize. Removes the identical SentNodeWaiter struct — good cleanup.

  • HashMap<&Jid, Jid> borrow (wacore/src/send.rs:350): Sound — devices: &[Jid] outlives the map. Eliminates clones in the hot path.

  • From<&Jid> for NodeValue (wacore/binary/src/node.rs:151): Clean impl, enables ~36 .clone() removals. The inner jid.clone() is necessary since NodeValue::Jid owns its data.

  • build_revoke_message() extraction (src/send.rs:183): 3 identical constructions → 1 function, no behavior change.

  • as_str() over to_string() for session lock keys (src/features/signal.rs, src/send.rs): Correct zero-copy replacements throughout.

  • write_protocol_address_to + cmp_for_lock_order (wacore/src/types/jid.rs): Good factoring — mapped_server() is now shared between the new helpers and the existing to_signal_address_string(), eliminating the duplicated match.

  • SenderKeyName::from_jid() migration (src/handlers/notification.rs, src/retry.rs, src/features/signal.rs): Removes manual .to_string() pairs at all 4 call sites.

  • flush_signal_cache_logged signature (src/client.rs:1359): Adding id: Option<&str> avoids the format!("message {}", info.id) allocation in the message dispatch hot path.

  • Benchmark (wacore/benches/lock_key_benchmark.rs): Nice addition comparing current vs proposed approaches with realistic JID distributions.

Minor nit

  • src/send.rs:733message_id.clone() is unnecessary since message_id isn't used after this line. Can be passed by value.

Summary

The dedup/sort equivalence mismatch is the only actionable item. Everything else is clean mechanical optimization. LGTM once the dedup_by fix is in.


jlucaso1 added 2 commits April 7, 2026 11:19
- Fix dedup_by to use cmp_for_lock_order (not PartialEq) so JIDs that
  differ only in agent/integrator but map to the same lock key are
  correctly deduplicated (prevents double-locking the same mutex)
- Remove unnecessary message_id.clone() in revoke_message (moved instead)
- Acquire session lock before deleting session in identity change handler
  (mirrors Signal::delete_sessions pattern, prevents race with concurrent
  encrypt/decrypt recreating the stale session)
- Acquire session lock before deleting session in reg-ID mismatch handler
  (same race condition)
@jlucaso1

jlucaso1 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 7, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 31s —— View job


Code Review: perf: reduce duplication and unnecessary allocations

  • Read PR diff and all changed files
  • Review correctness and safety of latest commits
  • Post review feedback

Well-structured PR — the DRY extractions are clean, borrow optimizations are sound, and the follow-up commits (8156bcd, 2a1614d) addressed the earlier review findings correctly. Here's what I found across all 4 commits:

Looks Good

  • resolve_waiters() extraction (src/client.rs:94): Clean free function. Removing the duplicate SentNodeWaiter struct is a nice touch.

  • build_session_lock_keys sort/dedup (src/send.rs:1499-1500): The dedup bug flagged by prior reviews is now fixed — dedup_by(|a, b| cmp_for_lock_order(a, b).is_eq()) correctly uses the same equivalence as the sort. Good.

  • Session lock for identity change (src/handlers/notification.rs:378-382): Correctly wraps delete_session + delete_identity under session_lock_for(). Matches the Signal::delete_sessions() pattern.

  • Session lock for reg-ID mismatch (src/retry.rs:235-238): Same fix applied to the retry path. Both correctness fixes are sound.

  • HashMap<&Jid, Jid> borrow (wacore/src/send.rs:350): Sound — devices: &[Jid] outlives the map. Avoids clones in the hot path.

  • From<&Jid> for NodeValue (wacore/binary/src/node.rs:151): Clean impl, enables ~36 .clone() removals. The inner jid.clone() is necessary since NodeValue::Jid owns its data.

  • build_revoke_message() extraction (src/send.rs:183): 3 identical constructions → 1 function, no behavior change. message_id is now passed by value in the production call site (line 733), fixing the unnecessary clone from the first iteration.

  • mapped_server() + write_protocol_address_to() + cmp_for_lock_order() (wacore/src/types/jid.rs): Good factoring — the server mapping logic is now shared across all three consumers.

  • flush_signal_cache_logged signature (src/client.rs:1359): id: Option<&str> avoids the format!("message {}", info.id) allocation in the hot path while preserving per-message log correlation.

  • NodeValue::to_jid() usage (wacore/src/send.rs:1336-1338): Replacing v.to_string().parse::<Jid>() with v.to_jid() in the status participants dedup is cleaner and avoids the round-trip.

Minor Nit

  • notification_timestampu64 as i64 wraps silently (src/handlers/notification.rs:1025): This was flagged in prior reviews and is pre-existing (not introduced by this PR), but since the PR consolidates more call sites onto this helper, it's worth a follow-up fix:

    fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
        node.attrs()
            .optional_u64("t")
            .and_then(|t| i64::try_from(t).ok())
            .and_then(|t| chrono::DateTime::from_timestamp(t, 0))
            .unwrap_or_else(chrono::Utc::now)
    }

    Not a blocker — values > i64::MAX from WhatsApp servers are practically impossible. Fix this →

Summary

LGTM — net -16 lines, all changes are mechanical deduplication, zero-copy improvements, or correctness fixes. The follow-up commits properly address the dedup/sort equivalence mismatch and session lock safety issues from earlier reviews.


@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

♻️ Duplicate comments (1)
src/handlers/notification.rs (1)

902-903: ⚠️ Potential issue | 🟡 Minor

notification_timestamp() still needs a checked cast.

These new call sites now depend on a helper that still does u64 as i64. Oversized t values become negative seconds, so from_timestamp() can return a bogus timestamp instead of taking the Utc::now() fallback.

🛠️ Proposed fix
 fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
     node.attrs()
         .optional_u64("t")
-        .map(|t| chrono::DateTime::from_timestamp(t as i64, 0).unwrap_or_else(chrono::Utc::now))
+        .and_then(|t| i64::try_from(t).ok())
+        .and_then(|t| chrono::DateTime::from_timestamp(t, 0))
         .unwrap_or_else(chrono::Utc::now)
 }

Also applies to: 992-992

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 902 - 903, The call to
notification_timestamp(node) relies on notification_timestamp performing an
unchecked u64 as i64 cast which can produce negative seconds for oversized
values; update notification_timestamp to perform a checked conversion (use
i64::try_from or TryInto) and handle failure by returning the Utc::now()
fallback (or saturating/clamping to i64::MAX if intended), ensuring callers like
notification_timestamp(...) and other call sites no longer receive bogus
negative seconds for from_timestamp(); update the function signature/return to
reflect the safe path so callers simply use the returned chrono::DateTime<Utc>.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/retry.rs`:
- Around line 997-999: The test helper build_retry_receipt sets the "to"
attribute using info.source.sender which diverges from production group routing
that uses info.source.chat; update build_retry_receipt (and the similar
occurrences around the other diffs) to set .attr("to", &info.source.chat) when
info.source represents a group chat (or otherwise mirror the production routing
decision), preserving id and type attributes and keeping sender usage only for
non-group routes so tests exercise group-addressing the same way as production.

---

Duplicate comments:
In `@src/handlers/notification.rs`:
- Around line 902-903: The call to notification_timestamp(node) relies on
notification_timestamp performing an unchecked u64 as i64 cast which can produce
negative seconds for oversized values; update notification_timestamp to perform
a checked conversion (use i64::try_from or TryInto) and handle failure by
returning the Utc::now() fallback (or saturating/clamping to i64::MAX if
intended), ensuring callers like notification_timestamp(...) and other call
sites no longer receive bogus negative seconds for from_timestamp(); update the
function signature/return to reflect the safe path so callers simply use the
returned chrono::DateTime<Utc>.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96dd26f0-7f0e-47ce-8583-07b349d2a37e

📥 Commits

Reviewing files that changed from the base of the PR and between 101c23d and 8156bcd.

📒 Files selected for processing (3)
  • src/handlers/notification.rs
  • src/retry.rs
  • src/send.rs

Comment thread src/retry.rs Outdated
- Fix test helper build_retry_receipt to route group retry receipts to
  info.source.chat (not sender), mirroring production logic at line 774
- Use i64::try_from instead of unchecked u64 as i64 in notification_timestamp
  (falls back to Utc::now on overflow instead of wrapping to negative)

@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.

Caution

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

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

217-223: ⚠️ Potential issue | 🟠 Major

Acquire the session lock before reading the cached session.

Line 235 starts the lock, but Lines 217-228 already read and compare the session state. A concurrent encrypt/decrypt can recreate or replace that session in the gap, and this branch will then delete the fresh state based on stale data.

Suggested fix
-                    let device_store = self.persistence_manager.get_device_arc().await;
-                    let device_guard = device_store.read().await;
+                    let lock = self.session_lock_for(signal_address.as_str()).await;
+                    let _guard = lock.lock().await;
+                    let device_store = self.persistence_manager.get_device_arc().await;
+                    let device_guard = device_store.read().await;

                     // Read session through cache to get consistent state
                     let session = self
                         .signal_cache
                         .get_session(&signal_address, &*device_guard.backend)
                         .await
@@
-                        let lock = self.session_lock_for(signal_address.as_str()).await;
-                        let _guard = lock.lock().await;
                         self.signal_cache.delete_session(&signal_address).await;
                         drop(_guard);

Based on learnings: "Use session_locks to serialize per-sender Signal encrypt/decrypt operations and message_enqueue_locks to serialize per-chat incoming message processing".

Also applies to: 225-240

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/retry.rs` around lines 217 - 223, Currently code calls
signal_cache.get_session(...) (via signal_cache.get_session and device_guard)
before acquiring the per-sender session lock, which allows a concurrent
encrypt/decrypt to replace the session and cause stale-state deletion; fix by
acquiring the per-sender session lock from session_locks (serialize per-sender
Signal ops) before calling signal_cache.get_session and hold that lock until
after you finish comparing/deleting/setting session state (also apply same
pattern around the other block using message_enqueue_locks for per-chat
processing), i.e., move the session_locks acquisition so it surrounds the calls
to signal_cache.get_session and the subsequent logic that may drop or replace
the session to prevent races.
♻️ Duplicate comments (1)
src/retry.rs (1)

996-1004: ⚠️ Potential issue | 🟡 Minor

Finish covering the routing fix with to assertions.

The helper now mirrors production, but Line 1044 and Line 1103 still never check to. The group-routing regression fixed here can come back without failing this test.

Suggested assertions
         let node = build_retry_receipt(&device_sync_info, &our_pn, &our_lid);
+        assert_eq!(
+            node.attrs.get("to").map(|v| v == "100000000000001@lid"),
+            Some(true),
+            "Device sync DM should target the sender JID"
+        );
         assert_eq!(
             node.attrs
                 .get("recipient")
                 .map(|v| v == "200000000000002@lid"),
@@
         let node = build_retry_receipt(&group_info, &our_pn, &our_lid);
+        assert_eq!(
+            node.attrs.get("to").map(|v| v == "123456789@g.us"),
+            Some(true),
+            "Group retry should target the chat JID"
+        );
         assert!(
             node.attrs.get("participant").is_some(),
             "Group should have participant"
         );

Also applies to: 1044-1059, 1103-1115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/retry.rs` around lines 996 - 1004, The tests still don't assert the "to"
routing set by receipt_to; update the test assertions that inspect the Node
built by NodeBuilder::new("receipt") (the builder with .attr("to",
receipt_to).attr("id", info.id.clone())) to explicitly check the "to" attribute
matches the expected value: when info.source.is_group is true assert "to" ==
info.source.chat, and when false assert "to" == info.source.sender; add these
"to" checks alongside the existing id/from/type assertions in the affected test
cases so the group-routing regression will fail the test if it reappears.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/retry.rs`:
- Around line 217-223: Currently code calls signal_cache.get_session(...) (via
signal_cache.get_session and device_guard) before acquiring the per-sender
session lock, which allows a concurrent encrypt/decrypt to replace the session
and cause stale-state deletion; fix by acquiring the per-sender session lock
from session_locks (serialize per-sender Signal ops) before calling
signal_cache.get_session and hold that lock until after you finish
comparing/deleting/setting session state (also apply same pattern around the
other block using message_enqueue_locks for per-chat processing), i.e., move the
session_locks acquisition so it surrounds the calls to signal_cache.get_session
and the subsequent logic that may drop or replace the session to prevent races.

---

Duplicate comments:
In `@src/retry.rs`:
- Around line 996-1004: The tests still don't assert the "to" routing set by
receipt_to; update the test assertions that inspect the Node built by
NodeBuilder::new("receipt") (the builder with .attr("to", receipt_to).attr("id",
info.id.clone())) to explicitly check the "to" attribute matches the expected
value: when info.source.is_group is true assert "to" == info.source.chat, and
when false assert "to" == info.source.sender; add these "to" checks alongside
the existing id/from/type assertions in the affected test cases so the
group-routing regression will fail the test if it reappears.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34aee1e4-f207-434a-8702-098656aceac9

📥 Commits

Reviewing files that changed from the base of the PR and between 8156bcd and c689977.

📒 Files selected for processing (2)
  • src/handlers/notification.rs
  • src/retry.rs

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.

1 participant