From 1a5071b4ab55324cd169ec25b8285c457ac71050 Mon Sep 17 00:00:00 2001 From: Bendik Lynghaug Date: Sun, 16 Aug 2026 15:50:24 +0200 Subject: [PATCH] Verify pushed content by polling observes, not one live watch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI showed the delivery verification timing out while the push itself succeeded: an observe watch opened while the peer's import is still creating the blob can miss the entry and never report again (locally the import always won the race, so a single complete bitfield arrived and the watch looked fine). Poll with fresh short-lived observe requests instead — every iteration reads the peer's current state, so the race disappears. Co-Authored-By: Claude Fable 5 --- varde-daemon/src/transfer.rs | 44 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/varde-daemon/src/transfer.rs b/varde-daemon/src/transfer.rs index e977d27..9160fb9 100644 --- a/varde-daemon/src/transfer.rs +++ b/varde-daemon/src/transfer.rs @@ -245,34 +245,36 @@ impl Transfer { Ok(bytes) } - /// Watch `hash` on the remote end of `conn` until its bitfield - /// reports the blob complete. Bounded: the peer has already received - /// the bytes, so verification is bookkeeping, not transfer. + /// Check `hash` on the remote end of `conn` until its bitfield + /// reports the blob complete. Polls with fresh observe requests + /// rather than holding one live watch: a watch opened while the + /// peer's import is still creating the blob can miss the entry and + /// stay silent forever, while a fresh request always reads current + /// state. Bounded: the peer has already received the bytes, so + /// verification is bookkeeping, not transfer. async fn wait_remote_complete( &self, conn: &iroh::endpoint::Connection, hash: Hash, ) -> Result<()> { - let observe = self.store.remote().observe( - conn.clone(), - iroh_blobs::protocol::ObserveRequest::new(hash), - ); - let mut observe = std::pin::pin!(observe); + let deadline = tokio::time::Instant::now() + Duration::from_secs(60); loop { - let next = tokio::time::timeout(Duration::from_secs(60), observe.next()) - .await - .map_err(|_| anyhow::anyhow!("verifying pushed content on the peer timed out"))?; - match next { - Some(bitfield) => { - let bitfield = bitfield.context("observing pushed content on the peer")?; - if bitfield.is_complete() { - return Ok(()); - } - } - None => { - anyhow::bail!("peer stopped reporting before the pushed content completed") - } + let observe = self.store.remote().observe( + conn.clone(), + iroh_blobs::protocol::ObserveRequest::new(hash), + ); + let mut observe = std::pin::pin!(observe); + match tokio::time::timeout(Duration::from_secs(10), observe.next()).await { + Ok(Some(Ok(bitfield))) if bitfield.is_complete() => return Ok(()), + // Present but not complete yet, or no snapshot in time: + // poll again from scratch. + Ok(Some(Ok(_))) | Ok(None) | Err(_) => {} + Ok(Some(Err(e))) => return Err(e).context("observing pushed content on the peer"), } + if tokio::time::Instant::now() >= deadline { + anyhow::bail!("verifying pushed content on the peer timed out"); + } + tokio::time::sleep(Duration::from_millis(500)).await; } }