Skip to content

fix(storage): serialize msg_secret reads through the db semaphore - #832

Merged
jlucaso1 merged 1 commit into
mainfrom
fix/msg-secret-reads-semaphore
Jun 10, 2026
Merged

fix(storage): serialize msg_secret reads through the db semaphore#832
jlucaso1 merged 1 commit into
mainfrom
fix/msg-secret-reads-semaphore

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Problem

get_msg_secret and get_msg_secret_with_ts ran their queries on raw spawn_blocking, bypassing the single-permit db_semaphore that every write path (with_retry) and most reads (with_semaphore) acquire. A read racing a write transaction hits the shared-cache table lock on in-memory stores and fails with SQLITE_LOCKED, which busy_timeout does not cover. Both call sites in msg_secret.rs treat a backend error as a miss, so an inbound add-on could spuriously fail to find its parent secret and surface undecrypted.

The write-behind drain from #831 made the overlap reachable in practice: the detached batch write races the very next message's secret lookup. The enc_comment_inbound_dispatches_body_with_parent_link pipeline test flaked at roughly coin-flip rate per process (reproduced on clean main, 2 failures in 6 runs), and the failure is silent because the lookup-miss path logs nothing. File-backed stores are shielded by WAL (readers don't block on writers), so this chiefly affects in-memory stores: the test suite and any embedded consumer using :memory:.

Change

Route both reads through with_semaphore, like the rest of the store. A read that overlaps a write now waits for the permit instead of erroring out as a phantom miss.

Tests

No new test: the existing pipeline test is the regression guard, and it goes from ~50% flake per process to 12/12 consecutive green runs with this fix. Full workspace suite green, cargo clippy --all-targets -- -D warnings clean.

Breaking

None.

get_msg_secret and get_msg_secret_with_ts ran on raw spawn_blocking,
bypassing the single-permit db semaphore every write path acquires. A
read racing a write transaction hits the shared-cache table lock on
in-memory stores (SQLITE_LOCKED, which busy_timeout does not cover) and
the caller treats the error as a missing secret, so an inbound addon
could spuriously fail to find its parent secret. The write-behind drain
made the overlap reachable: the enc-comment pipeline test flaked at
roughly coin-flip rate per process. File-backed stores were shielded by
WAL. Both reads now take the semaphore like the rest of the store;
the flaky test passes 12/12 consecutive runs against this fix.
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2223e178-204b-4f8b-93a0-f4d9a3809d06

📥 Commits

Reviewing files that changed from the base of the PR and between 1445899 and a1ca4a8.

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

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved data access reliability in the storage system to prevent potential consistency issues.

Walkthrough

Two database read methods for message secrets—get_msg_secret and get_msg_secret_with_ts—are moved from tokio::task::spawn_blocking to the store's with_semaphore(...) serialization wrapper. This eliminates read-after-write race failures in SQLite's in-memory shared-cache mode while consolidating error handling through the semaphore's existing await path.

Changes

Message Secret Read Serialization

Layer / File(s) Summary
Semaphore-serialized message secret reads
storages/sqlite-storage/src/sqlite_store.rs
get_msg_secret and get_msg_secret_with_ts construct owned string parameters and serialize database access through with_semaphore(...) instead of spawn_blocking, removing redundant error-mapping closures and delegating exception handling to the semaphore wrapper's .await? path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#127: This PR introduced the with_semaphore(...) helper itself; the current change applies that pattern to message secret read operations that previously used spawn_blocking.
  • oxidezap/whatsapp-rust#332: Extends semaphore-based serialization across additional SQLite operations in sqlite_store.rs, directly aligned with the refactoring pattern used here for secret reads.
  • oxidezap/whatsapp-rust#665: Adds batched put_msg_secrets persistence; this PR's semaphore-serialized reads prevent phantom-miss race conditions when looking up the secrets newly persisted by that PR's write path.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: routing msg_secret reads through the db semaphore to fix race condition issues.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, clearly explaining the problem, solution, and testing approach.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/msg-secret-reads-semaphore

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.

@github-actions

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,927 2,927 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,448 8,448 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,487 49,487 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,003 55,003 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,063 113,060 +0.0%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,620 1,656,623 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 641,908 641,900 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 852,909 852,869 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,989,726 1,989,786 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 734,144 734,147 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,284,099 1,288,177 -0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,147,002 4,177,145 -0.7%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 519,973 511,681 +1.6%
binary_benchmark::marshal_group::bench_marshal_allocating 40,690 40,690 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 40,743 40,743 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 61,909 61,909 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 38,953 38,953 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 40,796 40,796 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 5,144 5,144 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 5,174 5,174 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,954 6,954 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,339 528,339 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 527,963 527,963 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,211 529,211 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 4,769,980 4,769,980 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 4,769,621 4,769,621 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 12,643,701 12,643,701 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 28,069 28,069 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,888 672,888 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,845 3,845 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 47,180 47,180 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,871 3,871 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 47,241 47,241 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 65,610 65,610 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 234,591 234,591 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,579 8,579 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,140,791 4,138,084 +0.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,133 100,133 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,262 210,262 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,908 496,921 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 509,182 511,140 -0.4%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,979,830 11,973,664 +0.1%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,878,602 4,910,542 -0.7%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,351 2,043,351 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,404 37,414 -0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,648 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

@jlucaso1
jlucaso1 merged commit ef9c501 into main Jun 10, 2026
11 checks passed
@jlucaso1
jlucaso1 deleted the fix/msg-secret-reads-semaphore branch June 10, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant