use std::{sync::Arc, time::Duration}; use serde_json::json; use super::{filter_peer_addr_for_relay_mode, CoreInner}; use crate::{ handshake::{ DeliveryFailureReceipt, DeliveryReceipt, DeliveryReceiptResponse, HandshakeService, }, invitation::PendingDeliveryReceipt, ticket::parse_persisted_sender_address, }; const DELIVERY_RECEIPT_TIMEOUT: Duration = Duration::from_secs(5); const DELIVERY_RECEIPT_RETRY_INTERVAL: Duration = Duration::from_secs(5); const DELIVERY_RECEIPT_MAX_RETRY_INTERVAL: Duration = Duration::from_secs(5 * 60); impl CoreInner { pub(super) async fn spawn_delivery_receipt_task(self: &Arc) { let core = Arc::downgrade(self); let task = tokio::spawn(async move { let mut retry_interval = DELIVERY_RECEIPT_RETRY_INTERVAL; loop { let Some(core) = core.upgrade() else { break; }; let has_pending = core.deliver_pending_receipts().await; if has_pending { let notified = tokio::select! { () = core.delivery_receipt_notify.notified() => true, () = tokio::time::sleep(retry_interval) => false, }; if notified { retry_interval = DELIVERY_RECEIPT_RETRY_INTERVAL; } else { retry_interval = retry_interval .saturating_mul(2) .min(DELIVERY_RECEIPT_MAX_RETRY_INTERVAL); } } else { core.delivery_receipt_notify.notified().await; retry_interval = DELIVERY_RECEIPT_RETRY_INTERVAL; } } }); *self.delivery_receipt_task.lock().await = Some(task); } async fn deliver_pending_receipts(&self) -> bool { let receipts = match self.repository.list_pending_delivery_receipts().await { Ok(receipts) => receipts, Err(error) => { tracing::warn!(%error, "failed to load pending delivery receipts"); return true; } }; let has_pending = !receipts.is_empty(); for receipt in receipts { self.deliver_pending_receipt(receipt).await; } has_pending } async fn deliver_pending_receipt(&self, pending: PendingDeliveryReceipt) { let sender_addr = match parse_persisted_sender_address(&pending.sender_blob_ticket) { Ok(addr) => addr, Err(error) => { tracing::warn!(%error, request_id = %pending.request_id, "discarded invalid pending delivery receipt"); let _ = self .repository .delete_pending_delivery_receipt(&pending.request_id) .await; self.emit_transfer( pending.local_transfer_id, "receive", "delivery", "receipt-rejected", json!({ "reason": "invalid-sender-ticket" }), ); return; } }; let sender_addr = match filter_peer_addr_for_relay_mode( &sender_addr, self.relay_mode, &self.custom_relay_urls, ) { Ok(addr) => addr, Err(error) => { self.emit_transfer( pending.local_transfer_id, "receive", "delivery", "receipt-failed", json!({ "reason": error.to_string() }), ); return; } }; let client = HandshakeService::client(self.endpoint.clone(), sender_addr); let request_id = pending.request_id.clone(); let response = tokio::time::timeout(DELIVERY_RECEIPT_TIMEOUT, async { if let Some(reason) = pending.failure_reason { client .report_delivery_failure(DeliveryFailureReceipt { request_id, transfer_id: pending.sender_transfer_id, token: pending.token, reason, }) .await } else { client .report_delivery(DeliveryReceipt { request_id, transfer_id: pending.sender_transfer_id, token: pending.token, }) .await } }) .await; match response { Ok(Ok(DeliveryReceiptResponse::Recorded)) => { if let Err(error) = self .repository .delete_pending_delivery_receipt(&pending.request_id) .await { tracing::warn!(%error, request_id = %pending.request_id, "failed to clear recorded delivery receipt"); return; } self.emit_transfer( pending.local_transfer_id, "receive", "delivery", "receipt-recorded", json!({ "sender_transfer_id": pending.sender_transfer_id }), ); } Ok(Ok(DeliveryReceiptResponse::Rejected { reason })) => { let _ = self .repository .delete_pending_delivery_receipt(&pending.request_id) .await; self.emit_transfer( pending.local_transfer_id, "receive", "delivery", "receipt-rejected", json!({ "reason": reason }), ); } Ok(Err(error)) => self.emit_transfer( pending.local_transfer_id, "receive", "delivery", "receipt-failed", json!({ "reason": error.to_string() }), ), Err(_) => self.emit_transfer( pending.local_transfer_id, "receive", "delivery", "receipt-failed", json!({ "reason": "delivery receipt timed out" }), ), } } }