Skip to content

fix: codebase audit with bug fixes, race condition mitigations, and perf improvements - #511

Merged
jlucaso1 merged 9 commits into
mainfrom
fix/codebase-audit
Apr 11, 2026
Merged

fix: codebase audit with bug fixes, race condition mitigations, and perf improvements#511
jlucaso1 merged 9 commits into
mainfrom
fix/codebase-audit

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Comprehensive codebase audit addressing bugs, race conditions, resource leaks, and performance. 15 files changed across 8 commits.

Bugs fixed

Multi-account bootstrap broken on first boot (B1): create_new_device() now inserts with the configured device_id instead of relying on SQLite auto-increment. Previously SqliteStore::new_for_device(42) created row 1 on first boot but all reads targeted row 42, orphaning credentials.

Persistence saver halts permanently after transient SQLite contention (B2): save_device_data_for_device() and create_new_device() now use with_retry() for semaphore serialization and retry on SQLITE_BUSY/locked errors (up to 5 attempts with exponential backoff). Previously these were the only write paths bypassing with_retry, so 10 transient failures would permanently halt the background saver. Uses Arc<[u8]>/Arc<str> so retry clones are atomic increments. The upsert path now uses diesel excluded() references instead of duplicating all 20 column assignments.

ACK waiter leak disables keepalives (B3): spawn_phash_validation() now removes its waiter from response_waiters on timeout/error. Previously it leaked the entry, which suppressed keepalive pings for the rest of the connection. Both send paths also clean up the waiter if send_node() fails before spawning the validator.

App-state key-request dedupe suppresses retries for 24h after failure (B4): request_app_state_keys() now returns Result so callers detect failures. On error, dedup stamps are removed so keys can be retried on the next sync. Also returns Err when own JID is unavailable (instead of silent Ok(())) so stamps don't persist when the request was never attempted. The duplicated dedup+request logic (~28 lines in two call sites) was extracted into request_missing_keys_with_dedup().

Upload resume offset not bounds-checked (B5): Validates server resume offset before slicing upload data. Uses >= so offset-equal-to-length (zero bytes remaining) also falls back to full upload.

Server-sync not generation-bound (B6): Server-sync tasks now re-check connection_generation after the version comparison loop (which awaits DB reads), preventing stale tasks from continuing after a reconnect.

Error swallowing in sender key cache (B8): Replaced let _ = backend.take_sent_message(...) with explicit error logging.

Race conditions mitigated

Coordination cache eviction breaks serialization (R1): Increased default capacity for session_locks (2k to 10k), message_queues and message_enqueue_locks (2k to 5k). These hold live mutexes and channel senders; capacity eviction while a reference is held creates a duplicate lock for the same key.

Old per-chat workers survive reconnect (R2): Message queue workers capture connection_generation at spawn and check it before processing each message. Workers from a previous connection exit with a debug log instead of processing messages with stale crypto state.

Performance

Pre-allocate Vecs (P2): Replaced Vec::new() with Vec::with_capacity() in 13 sites where the final size is derivable from input (usync, community features, signal cache eviction, patch decoding, app state sync, pairing, derive macros).

Code quality

  • Normalized connection_generation atomic loads to Acquire ordering (was inconsistently SeqCst in notification.rs)
  • Updated CacheConfig doc comments to reflect current defaults
  • Eliminated unwrap() in phash validation callers via tuple destructuring

Behavioral changes

Cache capacity defaults changed: session_locks from 2,000 to 10,000; message_queues and message_enqueue_locks from 2,000 to 5,000. Users who explicitly set these values are unaffected.

Test plan

  • cargo clippy --all --tests passes with zero warnings
  • cargo test --all --lib passes
  • E2E tests with mock server
  • Manual test: multi-account bootstrap with new_for_device(N) where N > 1
  • Manual test: verify keepalive pings continue after phash timeout

Summary by CodeRabbit

  • Bug Fixes

    • App-state key requests now properly surface send failures and are retried on subsequent syncs.
    • Background workers and sync tasks abort when connection/crypto state changes to avoid stale processing.
    • Upload resumption validates server offsets to avoid corrupted resumes.
    • DB cleanup and acknowledgement paths now remove leaked waiter/state and log unexpected errors.
  • Performance

    • Increased coordination cache capacities for higher throughput.
    • Reduced allocations via targeted preallocation and buffer optimizations.
  • Tests

    • Added a test ensuring device creation respects configured IDs.

B1: create_new_device() now inserts with the configured device_id
instead of relying on auto-increment. Fixes first-boot mismatch
where new_for_device(N) created row 1 but reads targeted row N.

B2: save_device_data_for_device() now uses with_retry() for
semaphore serialization and retry on SQLITE_BUSY/locked errors.
Previously bypassed this path, allowing 10 transient failures to
permanently halt the background saver. Uses Arc<[u8]>/Arc<str>
so retry clones are atomic increments, not deep copies.

B3: spawn_phash_validation() now removes its waiter from
response_waiters on timeout/error. Previously leaked the entry,
which suppressed keepalive pings for the rest of the connection.
Callers restructured to eliminate unwrap() on phash Option.
request_app_state_keys() now returns Result so callers can detect
failure. Both call sites (sync_collections_batched and
sync_single_collection) remove the dedup stamps on error, allowing
the keys to be retried on the next sync instead of being suppressed
for 24 hours.
R1: Increase default capacity for session_locks (2k -> 10k),
message_queues and message_enqueue_locks (2k -> 5k) to reduce
the risk of capacity eviction creating duplicate locks for the
same key.

R2: Message queue workers now capture connection_generation at
spawn and check it before processing each message. Workers from
a previous connection exit immediately instead of processing
messages with stale crypto state.
B5: Validate server resume offset before slicing upload data.
Prevents panic if the server returns an offset exceeding the
data length; falls back to full upload with a warning.

B6: Server-sync tasks now re-check connection_generation after
the version comparison loop (which awaits DB reads). Prevents
stale sync tasks from continuing after a reconnect occurred
during the version filtering step.
Replace Vec::new() with Vec::with_capacity() in 13 sites across
the codebase where the final size is derivable from input length:

- src/usync.rs: device list fetching
- src/features/community.rs: link/unlink/participant results
- wacore/src/store/signal_cache.rs: eviction candidate lists
- wacore/appstate/src/patch_decode.rs: patch decoding
- wacore/src/appstate_sync.rs: missing key collection
- wacore/src/pair.rs: pairing message buffer (64+32+32 bytes)
- wacore/derive/src/lib.rs: derive macro field/variant lists
B8: Replace `let _ = backend.take_sent_message(...)` with an
explicit error log. Previously, DB deletion failures during
cache-hit cleanup were silently ignored.
@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Increase several cache defaults, consolidate and error-propagate app-state key requests, add connection-generation checks to stop stale workers, unify ack/waiter lifecycle with cleanup on error/timeout, validate upload resume offsets, refactor SQLite writes to use Arc-backed binds and centralized retry, and add multiple small preallocation optimizations.

Changes

Cohort / File(s) Summary
Cache Configuration
src/cache_config.rs
Bumped defaults: session_locks_capacity 2_000→10_000, message_queues_capacity 2_000→5_000, message_enqueue_locks_capacity 2_000→5_000; docs updated.
App-State Request Handling
src/client.rs
request_app_state_keys signature changed to return Result<(), anyhow::Error>; added request_missing_keys_with_dedup helper; callers now await and remove failed requests so keys can be retried.
Send / Ack Lifecycle
src/send.rs
Unified ack tuple (ack_receiver, phash, message_id); spawn_phash_validation gains message_id; on send failure or phash-timeout, remove response_waiters entries to avoid leaking waiters.
Message Worker / Notification Tasks
src/handlers/message.rs, src/handlers/notification.rs
Capture spawn-time connection_generation and re-check (atomic Acquire) inside worker/task loops; exit when generation differs to avoid using stale connection/crypto state.
SQLite Storage & Retry Refactor
storages/sqlite-storage/src/sqlite_store.rs
Switch bind params to Arc<[u8]>/Arc<str> & option-Arc; use with_retry(...) wrapper for DB ops instead of raw spawn_blocking/sql_query; explicitly set/return configured device::id.
Upload Resume Validation
src/upload.rs
Check server byte_offset vs enc.data_to_upload.len(); ignore out-of-range offsets (warn) or slice valid offsets and set file_offset.
DB Cleanup Logging
src/client/sender_keys.rs
Capture result of background take_sent_message and log a warning with {cs}:{mid} if it errors.
Preallocation & Micro-optimizations
src/features/community.rs, src/usync.rs, wacore/appstate/src/patch_decode.rs, wacore/derive/src/lib.rs, wacore/src/appstate_sync.rs, wacore/src/pair.rs, wacore/src/store/signal_cache.rs
Replace Vec::new() with Vec::with_capacity(...) and reuse child refs where sizes are known; allocation/efficiency changes only (no behavioral changes).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ResponseWaiters
  participant Transport as StanzaTransport
  participant PhashWorker as PhashValidator

  Client->>ResponseWaiters: register waiter (message_id, phash, ack_rx)
  Client->>Transport: send_node(stanza with id)
  alt send_node succeeded
    Transport->>ResponseWaiters: deliver ack node (message_id, phash)
    ResponseWaiters->>PhashWorker: notify ack receiver
    PhashWorker->>Client: validate phash -> success (remove waiter)
  else send_node failed
    Client->>ResponseWaiters: remove waiter (message_id)
    Client-->>Client: return/send error
  end

  note over PhashWorker,ResponseWaiters: on timeout PhashWorker removes waiter(message_id) before exiting
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I hopped through caches, buffers wrapped in Arc,
I checked each generation so stale tasks stay in the dark.
Waiters tidied, offsets bounded, retries set to try once more,
Vectors preallocated, fewer reallocs to store—
A rabbit cheers these tidy changes, then hops out the door.

🚥 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 title accurately summarizes the main changes: a comprehensive codebase audit with bug fixes, race condition mitigations, and performance improvements, matching the 15-file changeset across multiple categories.
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 fix/codebase-audit

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

Copy link
Copy Markdown
Collaborator Author

@claude review check also if changes can get some DRY and find possible regressions/bugs

@claude

claude Bot commented Apr 11, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 6m 34s —— View job


Review: Bug fixes, DRY opportunities, and regression check

  • Read all 15 changed files and understand the diff
  • Check for bugs and possible regressions
  • Identify DRY (Don't Repeat Yourself) opportunities
  • Post comprehensive review

Overall this is a solid audit PR — the fixes are well-motivated and the commit organization is clean. Here are specific findings:


Potential Bugs / Regressions

1. Upload offset bounds check: > should be >=src/upload.rs:168

if offset > enc.data_to_upload.len() {

Should be >=. If offset == enc.data_to_upload.len(), the slice &enc.data_to_upload[offset..] would be zero bytes — uploading nothing instead of falling back to a full upload. Currently this can't fire because parse_upload_progress (line 53) already validates offset < total_size where total_size == enc.data_to_upload.len(), but the defensive check should match:

if offset >= enc.data_to_upload.len() {

Fix this →

2. Stale message workers silently drop queued messagessrc/handlers/message.rs:89

When a worker detects a generation change and breaks, any messages already buffered in the rx channel are silently dropped. This is correct behavior (stale crypto state shouldn't process messages), but a debug log noting that messages were dropped would help with troubleshooting:

if client_for_worker
    .connection_generation
    .load(std::sync::atomic::Ordering::Acquire)
    != spawn_generation
{
    log::debug!(target: "MessageQueue", "Stale worker exiting; remaining messages will be redelivered by server");
    break;
}

This is minor — not a bug, just an observability gap.

3. Inconsistent atomic ordering between handlersmessage.rs vs notification.rs

src/handlers/message.rs:71,86 uses Ordering::Acquire while src/handlers/notification.rs:107,112,155 uses Ordering::SeqCst for the same connection_generation atomic. The store side (client.rs:1912) uses SeqCst.

Both are correct — Acquire is sufficient to pair with a SeqCst store — but the inconsistency is a code smell. I'd suggest using Acquire consistently for loads (matching sessions.rs:57,70 and the rest of the codebase) since SeqCst on the load side provides no additional guarantees here.


DRY Opportunities

1. App-state missing-key-request pattern (high value)src/client.rs:2571-2598 and 2785-2812

These two blocks are ~28 lines of identical code: build to_request from missing keys with dedup stamps, evict stale entries, call request_app_state_keys, remove stamps on failure. This should be extracted into a helper:

/// Request missing app-state keys with dedup stamps.
/// On send failure, removes stamps so keys can be retried next sync.
async fn request_missing_keys_with_dedup(&self, missing: Vec<Vec<u8>>) {
    if missing.is_empty() {
        return;
    }
    let mut to_request: Vec<Vec<u8>> = Vec::with_capacity(missing.len());
    let mut guard = self.app_state_key_requests.lock().await;
    let now = wacore::time::Instant::now();
    for key_id in missing {
        let hex_id = hex::encode(&key_id);
        let should = guard
            .get(&hex_id)
            .map(|t| t.elapsed() > std::time::Duration::from_secs(24 * 3600))
            .unwrap_or(true);
        if should {
            guard.insert(hex_id, now);
            to_request.push(key_id);
        }
    }
    guard.retain(|_, t| t.elapsed() < std::time::Duration::from_secs(24 * 3600));
    drop(guard);
    if !to_request.is_empty()
        && let Err(e) = self.request_app_state_keys(&to_request).await
    {
        warn!("Failed to send app state key request: {e}");
        let mut guard = self.app_state_key_requests.lock().await;
        for key_id in &to_request {
            guard.remove(&hex::encode(key_id));
        }
    }
}

Both call sites would collapse to self.request_missing_keys_with_dedup(missing).await;

Fix this →

2. save_device_data_for_device — 20 column assignments duplicatedsqlite_store.rs:363-384 and 389-408

The .values() and .do_update().set() lists repeat all 20 column assignments verbatim. Diesel's excluded() (already imported at line 7) can reference the INSERT values in the ON CONFLICT clause:

.on_conflict(device::id)
.do_update()
.set((
    device::lid.eq(excluded(device::lid)),
    device::pn.eq(excluded(device::pn)),
    device::registration_id.eq(excluded(device::registration_id)),
    // ... etc
))

This eliminates the entire second copy of the column values and removes the risk of the two lists drifting out of sync.

Fix this →


Code Quality Notes (non-blocking)

  • create_new_device doesn't use with_retry (sqlite_store.rs:422): This is inconsistent with save_device_data_for_device which was the motivation for B2. It's called once at first boot so the risk is low, but worth a comment or a follow-up.

  • Vec::with_capacity in community.rs:297-300: The refactoring to pre-extract edges_ref for the capacity hint is clean but slightly changes the control flow (from let-chain to and_then + separate if let). No behavioral change, just noting it.

  • Phash validation ack-registration pattern (send.rs:486-500 and 1113-1132): Both call sites follow the same shape: check phash → register ack → bundle → send → spawn validation. Minor duplication since the tuple shapes differ ((rx, phash) vs (rx, phash, msg_id)), but the first site could also explicitly pass request_id to spawn_phash_validation for consistency — it already has it in scope.


Verdict

The fixes are well-reasoned and correctly target real problems. The two DRY items above (key-request dedup helper and excluded() for upsert) would meaningfully reduce duplication and future drift risk. The >= bounds check fix is a one-character change worth making for defensive correctness.

@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: 3

Caution

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

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

69-90: ⚠️ Potential issue | 🔴 Critical

Worker exit can strand a closed per-chat sender in cache (message loss risk).

When Line 89 breaks on generation mismatch, the worker drops rx, but the cached tx for that chat_id can remain. Later enqueues hit a closed channel and fail, so messages may be dropped repeatedly until eviction.

Proposed fix
 let client_for_worker = client.clone();
+let chat_id_for_worker = chat_id.clone();
 let spawn_generation = client
     .connection_generation
-    .load(std::sync::atomic::Ordering::Acquire);
+    .load(std::sync::atomic::Ordering::SeqCst);

 // ...
 if client_for_worker
     .connection_generation
-    .load(std::sync::atomic::Ordering::Acquire)
+    .load(std::sync::atomic::Ordering::SeqCst)
     != spawn_generation
 {
+    // Ensure next enqueue recreates queue+worker for this chat.
+    client_for_worker
+        .message_queues
+        .invalidate(&chat_id_for_worker)
+        .await;
     break;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/message.rs` around lines 69 - 90, When the spawned worker
detects a connection_generation mismatch (compare between spawn_generation and
client_for_worker.connection_generation), it currently breaks and drops rx while
the cached tx for that chat_id can remain closed; update the worker closure so
that immediately before breaking on generation mismatch it explicitly
removes/invalidate the cached sender for that chat_id (e.g., call the cache
removal/invalidate helper on the client using the cloned chat_id or remove the
tx entry) to prevent future enqueues from hitting a closed channel; ensure the
worker has a cloned chat_id available and perform the removal atomically
relative to any cache helpers provided by the client to avoid races.
storages/sqlite-storage/src/sqlite_store.rs (1)

417-479: ⚠️ Potential issue | 🟠 Major

Serialize and retry device creation too.

Line 422 still writes through a raw spawn_blocking path, so first-boot device creation can still fail with SQLITE_BUSY/LOCKED while another write holds the database. That leaves the bootstrap path less resilient than save_device_data_for_device, even though both target the same SQLite file.

Suggested direction
 pub async fn create_new_device(&self) -> Result<i32> {
     use crate::schema::device;

-    let pool = self.pool.clone();
     let device_id = self.device_id;
-    tokio::task::spawn_blocking(move || -> Result<i32> {
-        let mut conn = pool
-            .get()
-            .map_err(|e| StoreError::Connection(e.to_string()))?;
-
-        let new_device = wacore::store::Device::new();
+    self.with_retry("create_new_device", || {
+        Box::new(move |conn: &mut SqliteConnection| {
+            let new_device = wacore::store::Device::new();

-        diesel::insert_into(device::table)
-            .values((
-                device::id.eq(device_id),
-                // ...
-            ))
-            .execute(&mut conn)
-            .map_err(|e| StoreError::Database(e.to_string()))?;
+            diesel::insert_into(device::table)
+                .values((
+                    device::id.eq(device_id),
+                    // ...
+                ))
+                .execute(conn)?;

-        Ok(device_id)
-    })
-    .await
-    .map_err(|e| StoreError::Database(e.to_string()))?
+            Ok(device_id)
+        })
+    })
+    .await
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storages/sqlite-storage/src/sqlite_store.rs` around lines 417 - 479,
create_new_device currently does writes inside tokio::task::spawn_blocking which
can race with other writers and get SQLITE_BUSY/LOCKED; make it use the same
serialized-retry write path as save_device_data_for_device. Replace the direct
spawn_blocking call in create_new_device with the existing serialize-and-retry
helper used by save_device_data_for_device (or extract that retry logic into a
shared function), move the diesel insert closure into that serialized block, and
ensure the retry loop detects SQLITE_BUSY/SQLITE_LOCKED and retries with backoff
before returning an error; keep the same return value (device_id) and preserve
all referenced symbols (new_device, noise_key_data, identity_key_data,
signed_pre_key_data) inside the closure.
🤖 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/cache_config.rs`:
- Around line 259-261: Update the struct docs for CacheConfig so the field
comments for session_locks_capacity, message_queues_capacity, and
message_enqueue_locks_capacity reflect the current default values (10000, 5000,
5000) instead of the outdated 2000; locate the comments adjacent to the
CacheConfig definition and replace the old default numbers/text with the new
defaults and, if present, any explanatory text to match the actual
initialization values used in the default implementation.

In `@src/client.rs`:
- Around line 2833-2841: The current request_app_state_keys method returns
Ok(()) when device_snapshot.pn is None (own_jid missing), which incorrectly
signals success and prevents caller retries; change this to return an error
instead of Ok by returning Err(anyhow::anyhow!("no own_jid available for
app-state request")) so callers will retry, or alternatively move any
dedupe-stamp insertion logic out of the caller and into request_app_state_keys
so stamps are only added after obtaining own_jid (use
persistence_manager.get_device_snapshot() and the own_jid match branch to gate
stamp insertion). Ensure the change updates request_app_state_keys and any
caller assumptions accordingly.

In `@src/send.rs`:
- Around line 491-500: The code registers an ack waiter (via
register_ack_waiter) before calling send_node, but if send_node(...) returns Err
the function returns early and never spawns the validator nor removes the
waiter, leaving a stale entry in response_waiters; fix by catching the send_node
result, and on Err remove/unregister the waiter from response_waiters (use the
same removal helper used in src/request.rs or explicitly remove by request_id)
before propagating the error, and apply the same change to the other similar
block (lines ~1123-1132) so spawn_phash_validation is only reached after a
successful send and no dead waiters remain.

---

Outside diff comments:
In `@src/handlers/message.rs`:
- Around line 69-90: When the spawned worker detects a connection_generation
mismatch (compare between spawn_generation and
client_for_worker.connection_generation), it currently breaks and drops rx while
the cached tx for that chat_id can remain closed; update the worker closure so
that immediately before breaking on generation mismatch it explicitly
removes/invalidate the cached sender for that chat_id (e.g., call the cache
removal/invalidate helper on the client using the cloned chat_id or remove the
tx entry) to prevent future enqueues from hitting a closed channel; ensure the
worker has a cloned chat_id available and perform the removal atomically
relative to any cache helpers provided by the client to avoid races.

In `@storages/sqlite-storage/src/sqlite_store.rs`:
- Around line 417-479: create_new_device currently does writes inside
tokio::task::spawn_blocking which can race with other writers and get
SQLITE_BUSY/LOCKED; make it use the same serialized-retry write path as
save_device_data_for_device. Replace the direct spawn_blocking call in
create_new_device with the existing serialize-and-retry helper used by
save_device_data_for_device (or extract that retry logic into a shared
function), move the diesel insert closure into that serialized block, and ensure
the retry loop detects SQLITE_BUSY/SQLITE_LOCKED and retries with backoff before
returning an error; keep the same return value (device_id) and preserve all
referenced symbols (new_device, noise_key_data, identity_key_data,
signed_pre_key_data) inside the closure.
🪄 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: 450e9a86-26ce-40fb-950e-8a3422f18bdd

📥 Commits

Reviewing files that changed from the base of the PR and between 80db1bb and 7a5d562.

📒 Files selected for processing (15)
  • src/cache_config.rs
  • src/client.rs
  • src/client/sender_keys.rs
  • src/features/community.rs
  • src/handlers/message.rs
  • src/handlers/notification.rs
  • src/send.rs
  • src/upload.rs
  • src/usync.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/appstate/src/patch_decode.rs
  • wacore/derive/src/lib.rs
  • wacore/src/appstate_sync.rs
  • wacore/src/pair.rs
  • wacore/src/store/signal_cache.rs

Comment thread src/cache_config.rs
Comment thread src/client.rs
Comment thread src/send.rs

@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: 7a5d562bc9

ℹ️ 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/handlers/message.rs
@github-actions

github-actions Bot commented Apr 11, 2026

Copy link
Copy Markdown

Benchmark Results

59 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,879 3,879 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 11,851 11,851 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,398 43,398 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 69,139 69,139 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 77,229 77,229 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,214 2,214 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,939 5,939 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 180,704 181,125 -0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 193,266 193,266 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 899,452 899,445 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 1,010,385 1,010,388 -0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,617,127 1,617,127 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 2,818,991 2,818,495 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 10,495,467 10,498,575 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 50,027,642 50,198,093 -0.3%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,499,433 12,694,922 -1.5%
binary_benchmark::marshal_group::bench_marshal_allocating 98,703 98,703 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 98,731 98,731 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 118,631 118,631 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 108,446 108,446 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 98,803 98,803 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 15,928 15,928 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 15,955 15,955 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 18,004 18,004 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 533,456 533,456 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 533,017 533,017 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 534,447 534,447 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 14,815,128 14,815,128 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 14,813,634 14,813,634 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 28,200,750 28,200,750 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,716 2,716 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 41,989 41,989 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 773 773 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 556,090 556,090 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 6,199 6,199 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 7,431 7,431 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 91,558 91,558 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 7,454 7,454 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 91,589 91,589 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 8,966 8,966 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 107,134 107,134 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 523,708 523,708 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 20,910 20,910 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,113,196 17,340,525 -1.3%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 162,069 162,069 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,512,660 5,512,660 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 163,021 163,085 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 298,493 298,493 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 713,231 713,231 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,692,873 12,545,485 +1.2%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,768,279 27,392,177 +1.4%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 124,662,673 124,732,113 -0.1%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,970 46,970 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,119,851 5,119,851 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 299,173 299,173 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,247,117 14,247,117 +0.0%
No significant changes detected.

…vice retry, docs

- Clean up ack waiters from response_waiters when send_node fails,
  preventing stale entries that suppress keepalives (both status
  and DM send paths)
- request_app_state_keys now returns Err when own JID is unavailable
  so callers remove dedup stamps instead of silently suppressing
  retries
- create_new_device now uses with_retry for semaphore serialization
  and SQLITE_BUSY retry, consistent with save_device_data_for_device
- Update CacheConfig doc comments to reflect current defaults
  (session_locks 10000, queues 5000)
- Upload resume offset: `>` → `>=` so offset==len falls back to full
  upload instead of uploading zero bytes
- Stale message worker: add debug log before break for observability
- Atomic ordering: normalize connection_generation loads to Acquire
  (was inconsistently SeqCst in notification.rs)
- Extract `request_missing_keys_with_dedup()` helper to deduplicate
  ~28 identical lines across sync_collections_batched and
  sync_single_collection
- Use diesel `excluded()` in save_device_data upsert to reference
  INSERT values instead of duplicating all 20 column assignments

@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: 4

🤖 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/client.rs`:
- Around line 2589-2597: Extract the duplicated "dedupe-stamp, send, and
rollback" logic into a shared async helper used by both call sites: create a
function (e.g., send_app_state_key_requests_with_rollback) that accepts (&self,
to_request: Vec<Vec<u8>>) and encapsulates the stamping into
self.app_state_key_requests.lock().await, calling
self.request_app_state_keys(&to_request).await, logging on Err(e) with the same
warn! text, and performing the rollback loop that removes hex::encode(key_id)
from the guard on failure; replace the two duplicated blocks (the one around
request_app_state_keys and the one at lines ~2803-2811) with calls to this new
helper so both paths use identical send-and-rollback behavior.

In `@src/send.rs`:
- Around line 1118-1132: The code currently uses unwrap_or_default() to produce
an empty-string id which can collide in response_waiters and leak; change the
stanza handling to require both phash and id using a let-chain pattern: replace
the current block that reads stanza_to_send.attrs().optional_string("phash") and
then uses .optional_string("id").map(...).unwrap_or_default() with an if let
Some(phash) = stanza_to_send.attrs().optional_string("phash").map(|s|
s.into_owned()) && let Some(msg_id) =
stanza_to_send.attrs().optional_string("id").map(|s| s.into_owned()) { let rx =
self.register_ack_waiter(&msg_id).await; Some((rx, phash, msg_id)) } else { None
}, thereby removing unwrap_or_default and ensuring register_ack_waiter and
response_waiters only use a real id; this also aligns with handle_ack_response
which looks up by id.

In `@storages/sqlite-storage/src/sqlite_store.rs`:
- Around line 417-476: Add a regression test that verifies the store uses the
configured device id path rather than a SQLite-generated rowid: use the
constructor new_for_device(..., N) to create a SqliteStore targeting device id
N, call create_new_device() on that store, then fetch the stored device (via the
existing load/get method used elsewhere, e.g., load_device or get_device_by_id)
and assert the returned device.id == N and that its persisted keys/fields match
the created values; this ensures create_new_device (and new_for_device) insert
using self.device_id and prevents falling back to SQLite row IDs.
- Around line 304-415: Both save_device_data_for_device and create_new_device
duplicate CoreDevice serialization and the diesel device::...eq(...) bindings;
extract that logic into a small shared helper (e.g., a DeviceDbModel or
bind_device_values function) that: serializes CoreDevice fields into the
Arc/Option<Arc<...>> forms (noise_key_data, identity_key_data,
signed_pre_key_data, account_data, push_name, new_lid, new_pn, etc.), and
exposes a single insert-values tuple and an identical update-set tuple (or a
method that returns both) that can be used inside the existing with_retry
closure and Box::new move closure; replace the duplicated serialization and the
long values(...).on_conflict(...).do_update().set(...) tuples in
save_device_data_for_device and create_new_device to call this helper instead so
both paths share the same binding/serialization logic.
🪄 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: 79d05353-b98b-48a9-b5e8-77c61b7372bb

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5d562 and 1c9e7c4.

📒 Files selected for processing (4)
  • src/cache_config.rs
  • src/client.rs
  • src/send.rs
  • storages/sqlite-storage/src/sqlite_store.rs

Comment thread src/client.rs Outdated
Comment thread src/send.rs
Comment thread storages/sqlite-storage/src/sqlite_store.rs
Comment thread storages/sqlite-storage/src/sqlite_store.rs
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review I fixed most of the previous issues, please check for more rooms for improvement or bugs introduced in the changes.

@jlucaso1 jlucaso1 changed the title fix: codebase audit — 13 bug fixes, race condition mitigations, and perf improvements fix: codebase audit with bug fixes, race condition mitigations, and perf improvements Apr 11, 2026
@claude

claude Bot commented Apr 11, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 4m 32s —— View job


Review: Post-fix follow-up

  • Read all changed files (diff against origin/main)
  • Check previous review items were addressed correctly
  • Look for new bugs or regressions introduced by fixes
  • Identify remaining DRY / improvement opportunities
  • Post comprehensive review

All previous review items have been addressed well. The fixes are solid. Here are the remaining findings:


Remaining issue (minor)

1. unwrap_or_default() still produces empty-string waiter keysrc/send.rs:1123-1127

let msg_id = stanza_to_send
    .attrs()
    .optional_string("id")
    .map(|s| s.into_owned())
    .unwrap_or_default();

If a stanza has phash but no id (unlikely but defensive code should handle it), this registers an ack waiter keyed on "". Multiple such stanzas would collide, and no ACK response will ever resolve a "" key — so the waiter leaks. Use a let-chain to require both attributes, matching the CLAUDE.md style guideline:

let ack = if let Some(phash) = stanza_to_send.attrs().optional_string("phash").map(|s| s.into_owned())
    && let Some(msg_id) = stanza_to_send.attrs().optional_string("id").map(|s| s.into_owned())
{
    let rx = self.register_ack_waiter(&msg_id).await;
    Some((rx, phash, msg_id))
} else {
    None
};

This also follows the collapsible-if / let-chain convention from the project guidelines.

Fix this →


Previous review items — verification

Item Status
Upload offset >>= ✅ Fixed (upload.rs:168)
Stale worker debug log ✅ Added (message.rs:89)
Inconsistent atomic ordering in notification.rs ✅ Normalized to Acquire
DRY: App-state key-request dedup helper ✅ Extracted request_missing_keys_with_dedup()
DRY: excluded() for upsert save_device_data_for_device uses excluded() references
create_new_device doesn't use with_retry ✅ Now uses with_retry
create_new_device uses explicit device_id ✅ Fixed — no more last_insert_rowid()
Ack waiter leak on send_node failure ✅ Both send paths clean up waiters
request_app_state_keys returns Ok(()) when no PN ✅ Now returns Err
Cache config doc comments stale defaults ✅ Updated to 10000/5000/5000

Observations (non-blocking, no action needed)

Stale worker tx in cache after generation break — When the message worker at message.rs:88-90 breaks on generation mismatch, the cached tx in message_queues remains. Subsequent sends via this tx will fail (SendError since rx is dropped) and messages are logged as warnings at line 114. They'll keep failing until the cache TTI expires and a new worker is created. This is acceptable — the server will redeliver messages on the new connection — but means there's a window where messages for that chat silently fail. The comment at lines 73-77 explains why invalidate() was intentionally avoided. Mentioning it here for awareness, not as a change request.

connection_generation loads in client.rs still use SeqCst — Lines 1956, 2053, 2143, 2209 in client.rs still load with SeqCst. The PR normalized notification.rs and message.rs to Acquire, but these pre-existing sites in client.rs were outside the diff scope. Acquire is sufficient for all of them (they pair with a SeqCst fetch_add store), but this is pre-existing code, not a regression.


Verdict

The fix commits addressed all previous review findings correctly. The only actionable item is the unwrap_or_default() → let-chain change in send.rs, which is low-severity but prevents a potential waiter leak edge case. Overall this is ready to merge after that one change (or with it acknowledged as a follow-up).


View job

- Phash ack registration now requires both phash AND id via let-chain
  instead of using unwrap_or_default(). Prevents empty-string key
  collisions in response_waiters when stanza has no id attribute.
- Add test_create_new_device_uses_configured_device_id that verifies
  new_for_device(42) creates row 42 (not row 1 via auto-increment)
  and that the data is loadable by the configured id.

@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: 43c3886213

ℹ️ 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/handlers/message.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/upload.rs`:
- Around line 167-173: The current parse_upload_progress hides invalid server
offsets by mapping offset >= total_size to NotFound, bypassing the warning in
the upload resume check; change parse_upload_progress so it does not convert
offsets >= total_size into NotFound but instead returns the parsed offset (or a
distinct enum variant like InvalidResume) so the existing guard that compares
byte_offset against enc.data_to_upload.len() (the block using offset =
byte_offset as usize and logging "Server resume offset {offset} exceeds data
length") can run and emit the warning; update callers of parse_upload_progress
accordingly to handle the returned offset/variant so the single
validation/logging point in the resume branch (the check around
enc.data_to_upload.len()) is always reached for out-of-range offsets.
🪄 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: 2705ff8b-c28c-4277-aa13-c8c964c97e77

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9e7c4 and 43c3886.

📒 Files selected for processing (5)
  • src/client.rs
  • src/handlers/message.rs
  • src/handlers/notification.rs
  • src/upload.rs
  • storages/sqlite-storage/src/sqlite_store.rs

Comment thread src/upload.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

♻️ Duplicate comments (1)
storages/sqlite-storage/src/sqlite_store.rs (1)

304-415: 🛠️ Refactor suggestion | 🟠 Major

Extract the device row serialization/binding into one helper.

save_device_data_for_device() and create_new_device() still duplicate the field serialization plus the long Diesel insert/update bindings for the device table. With this many columns, the next schema/default change will drift one path.

Also applies to: 420-478

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

In `@storages/sqlite-storage/src/sqlite_store.rs` around lines 304 - 415, The
device row serialization and Diesel binding logic duplicated in
save_device_data_for_device (the block passed to with_retry labeled
"save_device_data") and create_new_device should be extracted into a single
helper (e.g., build_device_row_values or bind_device_row) that: 1) takes the
DeviceData (or the already-serialized Arcs) and returns the tuple or closure of
values used in diesel::insert_into(device::table).values(...) and the matching
.set(...) for on_conflict; 2) centralizes the Arc serialization lines
(serialize_keypair, Arc::from conversions, to_string for lid/pn,
account/edge_routing_info mapping) so callers simply call the helper and pass
its result into their insert/update call; and 3) replace the duplicated blocks
in save_device_data_for_device and create_new_device to call this new helper to
keep a single source of truth for column bindings (ensure helper exposes the
same types used by diesel::values and excluded(...) set).
🤖 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`:
- Around line 658-661: The cleanup removes waiters by message_id only, which can
delete a newer retry's waiter when IDs are reused; change the
registration/cleanup to be instance-scoped: when inserting into
client.response_waiters from SendOptions.message_id, generate or return a unique
registration token/instance id (e.g., a small UUID or incrementing sequence) and
store the waiter under a compound key or as a struct containing that token
(e.g., map key (message_id, reg_id) or value with reg_id). Then in the failure
branch (the _ => block that currently calls
client.response_waiters.lock().await.remove(&message_id)) and the other similar
block (lines referenced around 1133-1136), remove only the entry that matches
both message_id and the registration token (or compare the stored waiter
instance equality) so a retry that registered a new waiter is not accidentally
removed.

---

Duplicate comments:
In `@storages/sqlite-storage/src/sqlite_store.rs`:
- Around line 304-415: The device row serialization and Diesel binding logic
duplicated in save_device_data_for_device (the block passed to with_retry
labeled "save_device_data") and create_new_device should be extracted into a
single helper (e.g., build_device_row_values or bind_device_row) that: 1) takes
the DeviceData (or the already-serialized Arcs) and returns the tuple or closure
of values used in diesel::insert_into(device::table).values(...) and the
matching .set(...) for on_conflict; 2) centralizes the Arc serialization lines
(serialize_keypair, Arc::from conversions, to_string for lid/pn,
account/edge_routing_info mapping) so callers simply call the helper and pass
its result into their insert/update call; and 3) replace the duplicated blocks
in save_device_data_for_device and create_new_device to call this new helper to
keep a single source of truth for column bindings (ensure helper exposes the
same types used by diesel::values and excluded(...) set).
🪄 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: d62822e7-7df0-4254-be87-e7b729b54556

📥 Commits

Reviewing files that changed from the base of the PR and between 43c3886 and e35f8b1.

📒 Files selected for processing (2)
  • src/send.rs
  • storages/sqlite-storage/src/sqlite_store.rs

Comment thread src/send.rs
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review, check if all claims are good, check if PR is ready to be merged or has any bug or showstopper

@claude

claude Bot commented Apr 11, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 3m 33s —— View job


Review: Final merge-readiness audit

  • Read all 15 changed files (full diff against origin/main)
  • Verify each claim in the PR description against the code
  • Check for bugs, regressions, or showstoppers
  • Check if previous review feedback was properly addressed
  • Provide final verdict

Claim verification

All claims in the PR description are verified and accurate:

Claim Verified Notes
B1create_new_device() uses configured device_id device::id.eq(device_id) at line 451, no more last_insert_rowid(). Regression test added.
B2save_device_data_for_device & create_new_device use with_retry Both now go through self.with_retry() with Arc-backed binds. excluded() used for upsert SET clause.
B3 — ACK waiter leak on timeout/error spawn_phash_validation removes waiter on timeout/error (send.rs:660). Both send paths clean up waiter on send_node failure (send.rs:498-500, send.rs:1133-1136).
B4 — App-state key-request dedup suppresses retries request_app_state_keys returns Result, request_missing_keys_with_dedup removes stamps on failure (client.rs:2803-2806). Returns Err when PN unavailable (client.rs:2818). DRY: two ~28-line blocks collapsed to single helper.
B5 — Upload resume offset bounds-checked Uses >= check (upload.rs:168). Falls back to full upload with warning.
B6 — Server-sync generation-bound Re-checks connection_generation after version comparison loop (notification.rs:153-159).
B8 — Error logging in sender key cache let _ = replaced with if let Err(e) = + log::warn! (sender_keys.rs:86-88).
R1 — Cache capacity increases session_locks_capacity 2k→10k, message_queues_capacity 2k→5k, message_enqueue_locks_capacity 2k→5k. Doc comments updated.
R2 — Stale workers exit on reconnect Workers capture spawn_generation and check before each message (message.rs:84-90). Debug log added.
P2 — Pre-allocate Vecs 13 sites confirmed across all listed files.
Atomic ordering normalization notification.rs now uses Acquire consistently for loads.

Previous review feedback status

All actionable items from both previous review rounds have been addressed:

Feedback Status
Upload >>= ✅ Fixed
Stale worker debug log ✅ Added
Atomic ordering consistency in notification.rs ✅ Normalized to Acquire
DRY: request_missing_keys_with_dedup ✅ Extracted
DRY: excluded() for upsert ✅ Applied
create_new_device uses with_retry ✅ Done
create_new_device uses explicit device_id ✅ Done
Waiter cleanup on send_node failure ✅ Both paths
request_app_state_keys returns Err when no PN ✅ Done
unwrap_or_default → let-chain for stanza id ✅ Fixed (send.rs:1118-1131)
device_id regression test ✅ Added

Bugs or showstoppers found

None. The code is correct throughout. Here are the minor observations I checked and confirmed as non-issues:

  1. Stale worker message drop (message.rs:89-90): When a worker breaks on generation mismatch, buffered messages are dropped. This is correct — the server will redeliver on the new connection. Processing with stale crypto state would be worse (could corrupt Signal ratchet). The comment at line 73-77 explains why invalidate() was avoided. The jlucaso1 responses on the earlier review are correct.

  2. Upload bounds guard "bypassed" (upload.rs:168 vs upload.rs:53): parse_upload_progress already filters offset >= total_size to NotFound, so the new guard at line 168 won't fire for those cases. But the guard is valid defense-in-depth — if total_size computation ever drifts from enc.data_to_upload.len(), it catches it. Not a bug.

  3. connection_generation loads still SeqCst in client.rs (lines 1956, 2053, 2143, 2209): Pre-existing, outside diff scope. Acquire would be sufficient but SeqCst is not incorrect — just slightly over-specified. Not a regression.

  4. Serialization duplication between save_device_data_for_device and create_new_device: Both have similar field serialization + insert bindings. Extracting a shared helper would reduce duplication, but it's a code quality nit, not a bug. The two functions have different binding shapes (one uses excluded() for upsert, the other is insert-only), so a shared helper would need somewhat complex generics. Fine as a follow-up.

Verdict

Ready to merge. All claims are verified, all review feedback has been addressed, no bugs or showstoppers found. The fixes are well-motivated, correctly implemented, and the code quality is high. The PR addresses real bugs (especially B1 device_id, B3 waiter leak, B4 dedup suppression) with minimal blast radius.


View job

@jlucaso1
jlucaso1 merged commit 0eda689 into main Apr 11, 2026
21 of 22 checks passed
@jlucaso1
jlucaso1 deleted the fix/codebase-audit branch April 11, 2026 04:58
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