diff --git a/src/message/retry.rs b/src/message/retry.rs index 5fa2532ec..a42e20a88 100644 --- a/src/message/retry.rs +++ b/src/message/retry.rs @@ -247,10 +247,16 @@ impl Client { reason ); } - let retry_sent = match self.send_retry_receipt(info, retry_count, reason).await { Ok(()) => { wacore::telemetry::retry_receipt(reason.as_str()); + if retry_count >= MAX_DECRYPT_RETRIES { + // Parity with WA Web's MessageHighRetryCount WAM event (id + // 3132): committed after the retry receipt is sent, not + // before — WAWebHandleMsgSendReceipt awaits sendRetryReceipt + // and only then calls maybePostMessageHighRetryCountMetric. + wacore::telemetry::high_retry(reason.as_str()); + } debug!( "Sent retry receipt #{} for message {} in chat {} from {} [{:?}]", retry_count, diff --git a/src/retry.rs b/src/retry.rs index eb727febf..dd4ff0fc1 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -201,7 +201,7 @@ fn build_retry_processing_key(chat: &Jid, message_id: &str, participant_jid: &Ji } impl Client { - #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.retry.handle_receipt", level = "debug", skip_all, fields(chat = %receipt.source.chat.observe(), sender = %receipt.source.sender.observe()), err(Debug)))] + #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.retry.handle_receipt", level = "debug", skip_all, fields(chat = %receipt.source.chat.observe(), sender = %receipt.source.sender.observe(), count = tracing::field::Empty), err(Debug)))] pub(crate) async fn handle_retry_receipt( self: &Arc, receipt: &Receipt, @@ -222,17 +222,24 @@ impl Client { .map(|v| v.as_str()) .and_then(|s| s.parse().ok()) .unwrap_or(1); + // Record the count on the span so retry-storm depth is aggregable per + // sender even when the cap refuses early below. + #[cfg(feature = "tracing")] + tracing::Span::current().record("count", retry_count); // Refuse to handle retries that have exceeded the maximum attempts. // This prevents infinite retry loops and matches WhatsApp Web's behavior. + // Logged at debug: remote-driven, expected and fully handled — WA Web + // emits this refusal via WALogger.LOG (informational), not WARN. if retry_count >= MAX_RETRY_COUNT { - warn!( + debug!( "Refusing retry #{} for message {} from {}: exceeds max attempts ({})", retry_count, message_id, receipt.source.sender.observe(), MAX_RETRY_COUNT ); + wacore::telemetry::retry_refused(); return Ok(()); } @@ -280,6 +287,14 @@ impl Client { .has_device(&info.requester.user, sender_device_id) .await; if !device_known { + // Parity with WA Web's MdRetryFromUnknownDevice WAM (id 2178), which + // commits here only — not from the shared inbound device sync, which + // schedule_unknown_device_sync is also called from elsewhere. + wacore::telemetry::retry_unknown_device(if sender_device_id == 0 { + "primary" + } else { + "companion" + }); self.schedule_unknown_device_sync(info.requester.to_non_ad(), receipt.offline) .await; } @@ -758,12 +773,18 @@ impl Client { .await { Ok(true) => { - warn!( - "Base key collision detected for {} at retry #{}. \ + // Informational, not WARN: this is the corrective action WA + // Web takes here too (WAWebUpdateLocalSignalSession logs the + // same-base-key delete via WALogger.LOG), and the three + // sibling branches of this routine already log at info. + info!( + "Base key collision detected for {} (msg {}) at retry #{}. \ Session hasn't been regenerated. Forcing fresh session.", wacore::types::jid::observe_protocol_address(&signal_address), + message_id, retry_count ); + wacore::telemetry::base_key_collision(); let _ = device_snapshot .backend .delete_base_key(addr_str, message_id) @@ -780,8 +801,9 @@ impl Client { } Ok(false) => { info!( - "Base key changed for {} at retry #{} - session regenerated", + "Base key changed for {} (msg {}) at retry #{} - session regenerated", wacore::types::jid::observe_protocol_address(&signal_address), + message_id, retry_count ); let _ = device_snapshot diff --git a/wacore/src/telemetry.rs b/wacore/src/telemetry.rs index de0ca3ed5..72527c88b 100644 --- a/wacore/src/telemetry.rs +++ b/wacore/src/telemetry.rs @@ -37,6 +37,26 @@ mod imp { pub fn retry_receipt(reason: &'static str) { counter!("wa_retry_receipt_total", "reason" => reason).increment(1); } + /// Retry receipt sent at the high-retry watermark (count >= MAX), by reason. + /// Mirrors WA Web's MessageHighRetryCount WAM event (id 3132). + pub fn high_retry(reason: &'static str) { + counter!("wa_high_retry_total", "reason" => reason).increment(1); + } + /// Retry receipt from a device not in our registry, by sender type + /// (`primary`/`companion`). Mirrors WA Web's MdRetryFromUnknownDevice WAM (id 2178). + pub fn retry_unknown_device(sender_type: &'static str) { + counter!("wa_retry_unknown_device_total", "sender_type" => sender_type).increment(1); + } + /// Retry refused at the MAX_RETRY loop guard. Aggregate health signal for a + /// chronically thrashing peer (WA Web logs this via WALogger.LOG, no WAM). + pub fn retry_refused() { + counter!("wa_retry_refused_total").increment(1); + } + /// Base-key collision that forced a fresh session (same base key after a + /// re-key, so the session was deleted and recreated). + pub fn base_key_collision() { + counter!("wa_base_key_collision_total").increment(1); + } /// IQ request completed, by result (`ok`/`timeout`/`error`). Emitted at the /// single request chokepoint, so it covers both raw and spec-based IQs. pub fn iq(result: &'static str) { @@ -110,6 +130,26 @@ mod imp { Unit::Count, "Retry receipts sent, by reason" ); + describe_counter!( + "wa_high_retry_total", + Unit::Count, + "Retry receipts sent at the high-retry watermark (count >= MAX), by reason" + ); + describe_counter!( + "wa_retry_unknown_device_total", + Unit::Count, + "Retries from devices not in our registry, by sender type" + ); + describe_counter!( + "wa_retry_refused_total", + Unit::Count, + "Retries refused at the MAX_RETRY loop guard" + ); + describe_counter!( + "wa_base_key_collision_total", + Unit::Count, + "Base-key collisions that forced a fresh session" + ); describe_counter!( "wa_iq_total", Unit::Count, @@ -180,6 +220,14 @@ mod imp { #[inline] pub fn retry_receipt(_reason: &'static str) {} #[inline] + pub fn high_retry(_reason: &'static str) {} + #[inline] + pub fn retry_unknown_device(_sender_type: &'static str) {} + #[inline] + pub fn retry_refused() {} + #[inline] + pub fn base_key_collision() {} + #[inline] pub fn iq(_result: &'static str) {} #[inline] pub fn reconnect() {}