diff --git a/crates/dht/src/arc_set.rs b/crates/dht/src/arc_set.rs index 8f4af2c6..84ce608b 100644 --- a/crates/dht/src/arc_set.rs +++ b/crates/dht/src/arc_set.rs @@ -68,18 +68,56 @@ impl ArcSet { } /// Get the intersection of two arc sets as a new [ArcSet]. + /// + /// # Example + /// + /// ```rust + /// use kitsune2_api::DhtArc; + /// use kitsune2_dht::ArcSet; + /// + /// # fn main() -> kitsune2_api::K2Result<()> { + /// use tracing::Instrument; + /// let arc_size = 1 << 23; + /// let arc_1 = DhtArc::Arc(0, 2 * arc_size - 1); + /// let arc_set_1 = ArcSet::new(arc_size, vec![arc_1])?; + /// + /// let arc_2 = DhtArc::Arc(arc_size, 4 * arc_size - 1); + /// let arc_set_2 = ArcSet::new(arc_size, vec![arc_2])?; + /// + /// assert_eq!(1, arc_set_1.intersection(&arc_set_2).covered_sector_count()); + /// # Ok(()) + /// # } + /// ``` pub fn intersection(&self, other: &Self) -> Self { ArcSet { inner: self.inner.intersection(&other.inner).copied().collect(), } } - pub(crate) fn includes_sector_id(&self, value: u32) -> bool { - self.inner.contains(&value) + /// The number of sectors covered by this arc set. + /// + /// # Example + /// + /// ```rust + /// use kitsune2_api::DhtArc; + /// use kitsune2_dht::ArcSet; + /// + /// # fn main() -> kitsune2_api::K2Result<()> { + /// let arc_size = 1 << 23; + /// let arc_1 = DhtArc::Arc(0, 2 * arc_size - 1); + /// let arc_2 = DhtArc::Arc(2 * arc_size, 4 * arc_size - 1); + /// let arc_set = ArcSet::new(arc_size, vec![arc_1, arc_2])?; + /// + /// assert_eq!(4, arc_set.covered_sector_count()); + /// # Ok(()) + /// # } + /// ``` + pub fn covered_sector_count(&self) -> usize { + self.inner.len() } - pub(crate) fn covered_sector_count(&self) -> usize { - self.inner.len() + pub(crate) fn includes_sector_id(&self, value: u32) -> bool { + self.inner.contains(&value) } } diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 0d011f24..3d967eb1 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -16,7 +16,7 @@ use crate::arc_set::ArcSet; use crate::PartitionedHashes; -use kitsune2_api::{DynOpStore, K2Result, OpId, StoredOp, Timestamp}; +use kitsune2_api::{DynOpStore, K2Error, K2Result, OpId, StoredOp, Timestamp}; use snapshot::{DhtSnapshot, SnapshotDiff}; pub mod snapshot; @@ -113,11 +113,22 @@ impl Dht { /// This is the entry point for comparing state with another DHT model. A minimal snapshot may /// be enough to check that two DHTs are in sync. The receiver should call [Dht::handle_snapshot] /// which will determine if the two DHTs are in sync or if a more detailed snapshot is required. + /// + /// # Errors + /// + /// Returns an error if there are no arcs to snapshot. If there is no overlap between the arc + /// sets of two DHT models then there is no point in comparing them because it will always + /// yield an empty diff. The [ArcSet::covered_sector_count] should be checked before calling + /// this method. pub async fn snapshot_minimal( &self, arc_set: &ArcSet, store: DynOpStore, ) -> K2Result { + if arc_set.covered_sector_count() == 0 { + return Err(K2Error::other("No arcs to snapshot")); + } + let (disc_top_hash, disc_boundary) = self.partition.disc_top_hash(arc_set, store).await?; @@ -166,6 +177,13 @@ impl Dht { /// /// The `arc_set` parameter is used to determine which arcs are relevant to the DHT model. This /// should be the [ArcSet::intersection] of the arc sets of the two DHT models to be compared. + /// + /// # Errors + /// + /// Returns an error if there are no arcs to snapshot. If there is no overlap between the arc + /// sets of two DHT models then there is no point in comparing them because it will always + /// yield an empty diff. The [ArcSet::covered_sector_count] should be checked before calling + /// this method. pub async fn handle_snapshot( &self, their_snapshot: &DhtSnapshot, @@ -173,6 +191,10 @@ impl Dht { arc_set: &ArcSet, store: DynOpStore, ) -> K2Result { + if arc_set.covered_sector_count() == 0 { + return Err(K2Error::other("No arcs to snapshot")); + } + let is_final = matches!( our_previous_snapshot, Some( diff --git a/crates/dht/src/dht/tests.rs b/crates/dht/src/dht/tests.rs index 3b4774bf..4ed665ba 100644 --- a/crates/dht/src/dht/tests.rs +++ b/crates/dht/src/dht/tests.rs @@ -51,6 +51,52 @@ async fn take_minimal_snapshot() { } } +#[tokio::test] +async fn cannot_take_minimal_snapshot_with_empty_arc_set() { + let current_time = Timestamp::now(); + let dht1 = DhtSyncHarness::new(current_time, DhtArc::Empty).await; + + let err = dht1 + .dht + .snapshot_minimal( + &ArcSet::new(SECTOR_SIZE, vec![dht1.arc]).unwrap(), + dht1.store.clone(), + ) + .await + .unwrap_err(); + assert_eq!("No arcs to snapshot (src: None)", err.to_string()); +} + +#[tokio::test] +async fn cannot_handle_snapshot_with_empty_arc_set() { + let current_time = Timestamp::now(); + let dht1 = DhtSyncHarness::new(current_time, DhtArc::Empty).await; + + // Declare a full arc to get a snapshot + let snapshot = dht1 + .dht + .snapshot_minimal( + &ArcSet::new(SECTOR_SIZE, vec![DhtArc::FULL]).unwrap(), + dht1.store.clone(), + ) + .await + .unwrap(); + + // Now try to compare that snapshot to ourselves with an empty arc set + let err = dht1 + .dht + .handle_snapshot( + &snapshot, + None, + &ArcSet::new(SECTOR_SIZE, vec![DhtArc::Empty]).unwrap(), + dht1.store.clone(), + ) + .await + .unwrap_err(); + + assert_eq!("No arcs to snapshot (src: None)", err.to_string()); +} + #[tokio::test] async fn empty_dht_is_in_sync_with_empty() { let current_time = Timestamp::now(); diff --git a/crates/dht/src/dht/tests/harness.rs b/crates/dht/src/dht/tests/harness.rs index a089354c..58a1e973 100644 --- a/crates/dht/src/dht/tests/harness.rs +++ b/crates/dht/src/dht/tests/harness.rs @@ -13,9 +13,9 @@ use std::sync::Arc; /// Intended to represent a single agent in a network, which knows how to sync with /// some other agent. pub(crate) struct DhtSyncHarness { - store: Arc, + pub(crate) store: Arc, pub(crate) dht: Dht, - arc: DhtArc, + pub(crate) arc: DhtArc, pub(crate) agent_id: AgentId, pub(crate) discovered_ops: HashMap>, }