diff --git a/ant-cli/src/commands/data/chunk.rs b/ant-cli/src/commands/data/chunk.rs index 9c7a0bf..9e4f278 100644 --- a/ant-cli/src/commands/data/chunk.rs +++ b/ant-cli/src/commands/data/chunk.rs @@ -130,6 +130,10 @@ impl PeerGetSummary { self.not_found += 1; "not_found".to_string() } + Err(DataError::NotFound(e)) => { + self.not_found += 1; + format!("not_found message={e}") + } Err(DataError::Timeout(e)) => { self.timeout += 1; format!("timeout message={e}") @@ -359,6 +363,10 @@ mod tests { assert_eq!(summary.record(&Ok(Some(chunk))), "found bytes=3"); assert_eq!(summary.record(&Ok(None)), "not_found"); + assert_eq!( + summary.record(&Err(DataError::NotFound("nothing at abcd".to_string()))), + "not_found message=nothing at abcd" + ); assert_eq!( summary.record(&Err(DataError::Timeout("slow".to_string()))), "timeout message=slow" @@ -373,7 +381,7 @@ mod tests { ); assert_eq!(summary.found, 1); - assert_eq!(summary.not_found, 1); + assert_eq!(summary.not_found, 2); assert_eq!(summary.timeout, 1); assert_eq!(summary.network_error, 1); assert_eq!(summary.error, 1); diff --git a/ant-core/src/data/client/data.rs b/ant-core/src/data/client/data.rs index a3a5b98..ba5fbf9 100644 --- a/ant-core/src/data/client/data.rs +++ b/ant-core/src/data/client/data.rs @@ -395,10 +395,11 @@ impl Client { /// /// # Errors /// - /// Returns an error if the chunk is not found or deserialization fails. + /// Returns [`Error::NotFound`] if no chunk exists at `address`; other + /// errors if retrieval or deserialization fails. pub async fn data_map_fetch(&self, address: &[u8; 32]) -> Result { let chunk = self.chunk_get(address).await?.ok_or_else(|| { - Error::InvalidData(format!( + Error::NotFound(format!( "DataMap chunk not found at {}", hex::encode(address) )) @@ -412,7 +413,8 @@ impl Client { /// /// # Errors /// - /// Returns an error if the chunk is not found or deserialization fails. + /// Returns [`Error::NotFound`] if no chunk exists at `address`; other + /// errors if retrieval or deserialization fails. pub async fn data_map_fetch_from_closest_peers( &self, address: &[u8; 32], @@ -422,7 +424,7 @@ impl Client { .chunk_get_from_closest_peers(address, peer_count.get()) .await? .ok_or_else(|| { - Error::InvalidData(format!( + Error::NotFound(format!( "DataMap chunk not found at {}", hex::encode(address) )) @@ -451,8 +453,9 @@ impl Client { /// /// # Errors /// - /// Returns an error if any chunk cannot be retrieved, if decryption fails, - /// or if a shrunk map must be resolved on a current-thread runtime. + /// Returns an error if any chunk cannot be retrieved (a chunk absent from + /// every queried peer surfaces as [`Error::NotFound`]), if decryption + /// fails, or if a shrunk map must be resolved on a current-thread runtime. pub async fn data_download(&self, data_map: &DataMap) -> Result { let root_data_map = self.resolve_root_data_map(data_map).await?; @@ -479,7 +482,7 @@ impl Client { // (Ok(None) -> Timeout is the load-shedding // signal for sustained close-group exhaustion). let chunk = self.chunk_get_observed(&address).await?.ok_or_else(|| { - Error::InvalidData(format!( + Error::NotFound(format!( "Missing chunk {} required for data reconstruction", hex::encode(address) )) @@ -552,7 +555,7 @@ impl Client { // The self-encryption fetcher may only yield `self_encryption::Error`. // Capture the underlying `ant-core` error out-of-band so a missing - // wrapper chunk surfaces as `Error::InvalidData` (matching the + // wrapper chunk surfaces as `Error::NotFound` (matching the // content-chunk path) and a network failure keeps its `Timeout` / // `Network` classification, instead of every resolution failure // flattening to `Error::Encryption`. @@ -566,7 +569,7 @@ impl Client { Ok(Some(chunk)) => Ok(chunk.content), Ok(None) => Err(record_wrapper_fetch_error( &mut fetch_error, - Error::InvalidData(format!( + Error::NotFound(format!( "Missing wrapper chunk {} required to resolve root DataMap", hex::encode(address) )), diff --git a/ant-core/src/data/client/mod.rs b/ant-core/src/data/client/mod.rs index 5214df9..224afaf 100644 --- a/ant-core/src/data/client/mod.rs +++ b/ant-core/src/data/client/mod.rs @@ -55,11 +55,12 @@ pub(crate) const PUT_TARGET_WIDTH: usize = 20; /// - `PartialUpload` -> `NetworkError` (literal capacity signal: some /// chunks could not be stored) /// - `AlreadyStored`, `Encryption`, `Crypto`, `Payment`, -/// `Serialization`, `InvalidData`, `SignatureVerification`, +/// `Serialization`, `InvalidData`, `NotFound`, `SignatureVerification`, /// `Config`, `InsufficientDiskSpace`, `CostEstimationInconclusive`, /// `Cancelled` -> `ApplicationError` (would happen on a perfectly /// healthy link; `Cancelled` is caller-initiated and must not be retried -/// as a transport failure) +/// as a transport failure; `NotFound` is a definitively absent record +/// reported over a working link, not a transport symptom) /// - `RemotePut` -> `ApplicationError` (the remote node responded with a /// structured rejection — the transport succeeded, so the node declined /// at the application layer; not a local capacity signal) @@ -83,6 +84,10 @@ pub(crate) fn classify_error(err: &Error) -> Outcome { | Error::Payment(_) | Error::Serialization(_) | Error::InvalidData(_) + // A definitively absent record, reported over a working link — + // the peers answered, there was just nothing stored there. Not a + // transport symptom, so it must not push the limiter down. + | Error::NotFound(_) | Error::SignatureVerification(_) | Error::Config(_) | Error::InsufficientDiskSpace(_) @@ -623,6 +628,13 @@ mod tests { Error::InvalidData("d".to_string()), Outcome::ApplicationError, ), + // A definitively absent record over a working link — the peers + // answered, nothing was stored there. Must NOT register as a + // capacity signal. + ( + Error::NotFound("missing".to_string()), + Outcome::ApplicationError, + ), ( Error::Serialization("s".to_string()), Outcome::ApplicationError, @@ -759,6 +771,7 @@ mod tests { | Error::Payment(_) | Error::Protocol(_) | Error::InvalidData(_) + | Error::NotFound(_) | Error::Serialization(_) | Error::Crypto(_) | Error::Io(_) diff --git a/ant-core/src/data/error.rs b/ant-core/src/data/error.rs index e0f49b7..8039e30 100644 --- a/ant-core/src/data/error.rs +++ b/ant-core/src/data/error.rs @@ -64,6 +64,17 @@ pub enum Error { #[error("invalid data: {0}")] InvalidData(String), + /// The requested record does not exist on the network. + /// + /// A well-formed address with nothing stored at it — e.g. a `DataMap` + /// chunk lookup or a reconstruction fetch that came back empty from + /// every queried peer. Distinct from [`Error::InvalidData`], which means + /// content WAS retrieved but is malformed or fails integrity checks, so + /// callers can show "not found — check the address" instead of a + /// caller-bug message. + #[error("not found: {0}")] + NotFound(String), + /// Serialization error. #[error("serialization error: {0}")] Serialization(String), @@ -230,6 +241,15 @@ mod tests { assert_eq!(err.to_string(), "invalid data: bad hash"); } + #[test] + fn test_display_not_found() { + let err = Error::NotFound("DataMap chunk not found at abcd".to_string()); + assert_eq!( + err.to_string(), + "not found: DataMap chunk not found at abcd" + ); + } + #[test] fn test_display_serialization() { let err = Error::Serialization("decode failed".to_string());