-
-
Notifications
You must be signed in to change notification settings - Fork 127
feat: add awaited pre-ACK message hook #919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,17 @@ impl Client { | |
| /// Flush the in-memory signal cache to the database backend. | ||
| /// Called after each message is decrypted or after encryption operations. | ||
| pub(crate) async fn flush_signal_cache(&self) -> Result<(), anyhow::Error> { | ||
| if self.parsed_message_pre_ack_hook.get().is_some() | ||
| && self | ||
| .pending_parsed_message_pre_ack_count | ||
| .load(Ordering::Acquire) | ||
| > 0 | ||
| { | ||
| return Err(anyhow::anyhow!( | ||
| "Signal cache flush deferred while parsed-message pre-ACK commit is pending" | ||
| )); | ||
|
Comment on lines
+68
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Don't scream
🤖 Prompt for AI Agents |
||
| } | ||
|
Comment on lines
+62
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift This is the part that has to work right: one stuck commit can wedge Signal persistence for the entire client, forever. The gate is global, not per-message: as long as
There's no bound, timeout, or escape hatch here. Please add one — e.g. a max pending age/count after which the entry is abandoned (and the message left for redelivery) so a single bad commit can't brick Signal persistence indefinitely. 🤖 Prompt for AI Agents |
||
|
|
||
| // Hold no device guard across the flush: this per-message batched SQLite | ||
| // write would otherwise block every concurrent Device write for its duration. | ||
| let backend = self | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,13 @@ impl Client { | |
| ), | ||
|
|
||
| undecryptable_dispatched: cache_config.undecryptable_dispatched.build_with_ttl(), | ||
| // Intentionally unbounded/no-TTL: an entry means Signal decrypt may | ||
| // have advanced volatile state for a message the application has | ||
| // not durably committed. Expiring it would allow a later Signal | ||
| // flush to make that advance durable and turn server redelivery | ||
| // into an ACKed duplicate without rerunning the hook. | ||
| pending_parsed_message_pre_ack: Cache::builder().build(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This cache is intentionally unbounded with no TTL, and it is never cleared in Prompt for AI agents |
||
| pending_parsed_message_pre_ack_count: AtomicUsize::new(0), | ||
|
Comment on lines
+195
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift The unbounded, never-cleared pending cache is the thing I'm worried about — let's make sure it can't grow forever. Look, I get the design: an entry means Signal state advanced for an uncommitted message, so expiring it would be wrong. But this cache has no capacity bound and is never cleared in 🤖 Prompt for AI Agents |
||
|
|
||
| offline_sync_metrics: Arc::new(OfflineSyncMetrics { | ||
| active: AtomicBool::new(false), | ||
|
|
@@ -226,6 +233,7 @@ impl Client { | |
| pairing_cancellation_tx: Arc::new(Mutex::new(None)), | ||
| pair_code_state: Arc::new(Mutex::new(wacore::pair_code::PairCodeState::default())), | ||
| custom_enc_handlers: std::sync::OnceLock::new(), | ||
| parsed_message_pre_ack_hook: std::sync::OnceLock::new(), | ||
| chatstate_handlers: Arc::new(RwLock::new(Vec::new())), | ||
| pdo_pending_requests: cache_config.pdo_pending_requests.build_with_ttl(), | ||
| pdo_requested: cache_config.pdo_requested.build_with_ttl(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,15 @@ impl Client { | |
| /// a consistent (generation, Arc) pair. Must be called from a non-async | ||
| /// context or inside a scoped block (MutexGuard is !Send). | ||
| pub(crate) fn swap_message_semaphore(&self, permits: usize) { | ||
| let permits = if self.parsed_message_pre_ack_hook.get().is_some() { | ||
| // A pre-ACK hook can intentionally fail after Signal decrypt has | ||
| // advanced volatile state. Keep message processing serial while the | ||
| // hook is installed so pending retry/flush deferral has one inbound | ||
| // message to reason about at a time. | ||
| 1 | ||
| } else { | ||
| permits | ||
| }; | ||
|
Comment on lines
+24
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial This pins the whole receive pipeline to single-threaded for the client's entire lifetime — make sure consumers understand the cost. Forcing 🤖 Prompt for AI Agents |
||
| let mut guard = match self.message_processing_semaphore.lock() { | ||
| Ok(g) => g, | ||
| Err(poisoned) => poisoned.into_inner(), | ||
|
|
@@ -415,9 +424,13 @@ impl Client { | |
| }; | ||
| match tag { | ||
| "receipt" | "notification" | "call" => true, | ||
| "message" => from | ||
| .to_jid() | ||
| .is_some_and(|j| j.is_newsletter() || j.is_status_broadcast()), | ||
| "message" => { | ||
| if self.parsed_message_pre_ack_hook.get().is_some() { | ||
| return false; | ||
| } | ||
| from.to_jid() | ||
| .is_some_and(|j| j.is_newsletter() || j.is_status_broadcast()) | ||
| } | ||
| _ => false, | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: This returns an
Errfor what is expected, routine backpressure (a deferred flush while a pre-ACK commit is pending). Theflush_signal_cache_loggedcaller will log it aterror!level on every deferred call, creating noisy false-alarm entries during normal operation. Either return a distinguishable "deferred" variant that the logged wrapper can downgrade todebug!/trace!, or use a dedicated error type that callers can match on to adjust log severity.Prompt for AI agents