Skip to content
8 changes: 7 additions & 1 deletion src/message/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 27 additions & 5 deletions src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>,
receipt: &Receipt,
Expand All @@ -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(());
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
48 changes: 48 additions & 0 deletions wacore/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {}
Expand Down
Loading