use crate::HashPartition;
use crate::arc_set::ArcSet;
use kitsune2_api::{
BoxFut, DynOpStore, K2Error, K2Result, OpId, StoredOp, Timestamp,
};
use snapshot::{DhtSnapshot, SnapshotDiff};
use std::fmt::Formatter;
pub(crate) mod snapshot;
#[cfg(test)]
mod tests;
pub struct Dht {
partition: HashPartition,
store: DynOpStore,
op_count: u64,
}
impl std::fmt::Debug for Dht {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dht")
.field("partition", &self.partition)
.field("op_count", &self.op_count)
.finish()
}
}
#[derive(Debug)]
pub enum DhtSnapshotNextAction {
Identical,
CannotCompare,
NewSnapshot(DhtSnapshot),
NewSnapshotAndHashList(DhtSnapshot, Vec<OpId>),
HashList(Vec<OpId>),
}
#[cfg_attr(feature = "mockall", mockall::automock)]
pub trait DhtApi: 'static + Send + Sync + std::fmt::Debug {
fn next_update_at(&self) -> Timestamp;
fn update(&mut self, current_time: Timestamp) -> BoxFut<'_, K2Result<()>>;
fn inform_ops_stored(
&mut self,
stored_ops: Vec<StoredOp>,
) -> BoxFut<'_, K2Result<()>>;
fn op_count(&self) -> u64;
fn snapshot_minimal(
&self,
arc_set: ArcSet,
) -> BoxFut<'_, K2Result<DhtSnapshot>>;
fn handle_snapshot(
&self,
their_snapshot: DhtSnapshot,
our_previous_snapshot: Option<DhtSnapshot>,
arc_set: ArcSet,
max_op_data_bytes: i32,
) -> BoxFut<'_, K2Result<(DhtSnapshotNextAction, u32)>>;
}
impl DhtApi for Dht {
fn next_update_at(&self) -> Timestamp {
self.partition.next_update_at()
}
fn update(&mut self, current_time: Timestamp) -> BoxFut<'_, K2Result<()>> {
Box::pin(async move {
self.partition
.update(self.store.clone(), current_time)
.await
})
}
fn inform_ops_stored(
&mut self,
stored_ops: Vec<StoredOp>,
) -> BoxFut<'_, K2Result<()>> {
Box::pin(async move {
let count = stored_ops.len() as u64;
self.partition
.inform_ops_stored(self.store.clone(), stored_ops)
.await?;
self.op_count += count;
Ok(())
})
}
fn op_count(&self) -> u64 {
self.op_count
}
fn snapshot_minimal(
&self,
arc_set: ArcSet,
) -> BoxFut<'_, K2Result<DhtSnapshot>> {
Box::pin(async move {
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, self.store.clone())
.await?;
Ok(DhtSnapshot::Minimal {
disc_top_hash,
disc_boundary,
ring_top_hashes: self.partition.ring_top_hashes(&arc_set),
})
})
}
fn handle_snapshot(
&self,
their_snapshot: DhtSnapshot,
our_previous_snapshot: Option<DhtSnapshot>,
arc_set: ArcSet,
max_op_data_bytes: i32,
) -> BoxFut<'_, K2Result<(DhtSnapshotNextAction, u32)>> {
Box::pin(async move {
if arc_set.covered_sector_count() == 0 {
return Err(K2Error::other("No arcs to snapshot"));
}
let is_final = matches!(
our_previous_snapshot,
Some(
DhtSnapshot::DiscSectorDetails { .. }
| DhtSnapshot::RingSectorDetails { .. }
)
);
let our_snapshot = match &their_snapshot {
DhtSnapshot::Minimal { .. } => {
self.snapshot_minimal(arc_set.clone()).await?
}
DhtSnapshot::DiscSectors { .. } => {
self.snapshot_disc_sectors(&arc_set).await?
}
DhtSnapshot::DiscSectorDetails {
disc_sector_hashes, ..
} => match our_previous_snapshot {
Some(snapshot @ DhtSnapshot::DiscSectorDetails { .. }) => {
#[cfg(test)]
{
let would_have_used = self
.snapshot_disc_sector_details(
disc_sector_hashes
.keys()
.cloned()
.collect(),
&arc_set,
self.store.clone(),
)
.await?;
assert_eq!(would_have_used, snapshot);
}
snapshot
}
_ => {
self.snapshot_disc_sector_details(
disc_sector_hashes.keys().cloned().collect(),
&arc_set,
self.store.clone(),
)
.await?
}
},
DhtSnapshot::RingSectorDetails {
ring_sector_hashes, ..
} => {
match our_previous_snapshot {
Some(
snapshot @ DhtSnapshot::RingSectorDetails { .. },
) => {
#[cfg(test)]
{
let would_have_used = self
.snapshot_ring_sector_details(
ring_sector_hashes
.keys()
.cloned()
.collect(),
&arc_set,
)?;
assert_eq!(would_have_used, snapshot);
}
snapshot
}
_ => self.snapshot_ring_sector_details(
ring_sector_hashes.keys().cloned().collect(),
&arc_set,
)?,
}
}
};
match our_snapshot.compare(&their_snapshot) {
SnapshotDiff::Identical => {
Ok((DhtSnapshotNextAction::Identical, 0))
}
SnapshotDiff::CannotCompare => {
Ok((DhtSnapshotNextAction::CannotCompare, 0))
}
SnapshotDiff::DiscMismatch => Ok((
DhtSnapshotNextAction::NewSnapshot(
self.snapshot_disc_sectors(&arc_set).await?,
),
0,
)),
SnapshotDiff::DiscSectorMismatches(mismatched_sectors) => Ok((
DhtSnapshotNextAction::NewSnapshot(
self.snapshot_disc_sector_details(
mismatched_sectors,
&arc_set,
self.store.clone(),
)
.await?,
),
0,
)),
SnapshotDiff::DiscSectorSliceMismatches(
mismatched_slice_indices,
) => {
let mut mismatched_slice_indices = mismatched_slice_indices
.into_iter()
.collect::<Vec<_>>();
mismatched_slice_indices
.sort_by_key(|(sector_index, _)| *sector_index);
let mut out = Vec::new();
let mut used_bytes = 0;
for (sector_index, mut missing_slices) in
mismatched_slice_indices
{
if used_bytes as i32 >= max_op_data_bytes {
break;
}
let Ok(arc) = self
.partition
.dht_arc_for_sector_index(sector_index)
else {
tracing::error!(
"Sector index {} out of bounds, ignoring",
sector_index
);
continue;
};
missing_slices.sort();
for missing_slice in missing_slices {
let Ok((start, end)) = self
.partition
.time_bounds_for_full_slice_index(
missing_slice,
)
else {
tracing::error!(
"Missing slice {} out of bounds, ignoring",
missing_slice
);
continue;
};
let (op_ids, ub) = self
.store
.retrieve_op_hashes_in_time_slice(
arc, start, end,
)
.await?;
if (used_bytes + ub) as i32 <= max_op_data_bytes {
out.extend(op_ids);
used_bytes += ub;
}
}
}
Ok(if is_final {
(DhtSnapshotNextAction::HashList(out), used_bytes)
} else {
(
DhtSnapshotNextAction::NewSnapshotAndHashList(
our_snapshot,
out,
),
used_bytes,
)
})
}
SnapshotDiff::RingMismatches(mismatched_rings) => Ok((
DhtSnapshotNextAction::NewSnapshot(
self.snapshot_ring_sector_details(
mismatched_rings,
&arc_set,
)?,
),
0,
)),
SnapshotDiff::RingSectorMismatches(mismatched_sectors) => {
let mut mismatched_sectors =
mismatched_sectors.into_iter().collect::<Vec<_>>();
mismatched_sectors
.sort_by_key(|(ring_index, _)| *ring_index);
let mut out = Vec::new();
let mut used_bytes = 0;
'outer: for (ring_index, mut missing_sectors) in
mismatched_sectors
{
missing_sectors.sort();
for sector_index in missing_sectors {
if used_bytes as i32 >= max_op_data_bytes {
break 'outer;
}
let Ok(arc) = self
.partition
.dht_arc_for_sector_index(sector_index)
else {
tracing::error!(
"Sector index {} out of bounds, ignoring",
sector_index
);
continue;
};
let Ok((start, end)) = self
.partition
.time_bounds_for_partial_slice_index(
ring_index,
)
else {
tracing::error!(
"Partial slice index {} out of bounds, ignoring",
ring_index
);
continue;
};
let (op_ids, ub) = self
.store
.retrieve_op_hashes_in_time_slice(
arc, start, end,
)
.await?;
if (used_bytes + ub) as i32 <= max_op_data_bytes {
tracing::debug!(
"Accepting op batch in sector: {}, ring: {}",
sector_index,
ring_index
);
out.extend(op_ids);
used_bytes += ub;
} else {
tracing::info!(
"No space for batch of ops from sector {}, needs {} bytes",
sector_index,
ub
);
}
}
}
Ok(if is_final {
(DhtSnapshotNextAction::HashList(out), used_bytes)
} else {
(
DhtSnapshotNextAction::NewSnapshotAndHashList(
our_snapshot,
out,
),
used_bytes,
)
})
}
}
})
}
}
impl Dht {
#[tracing::instrument(level = "debug", skip(store))]
pub async fn try_from_store(
current_time: Timestamp,
store: DynOpStore,
) -> K2Result<Dht> {
let op_count = store.query_total_op_count().await?;
Ok(Dht {
partition: HashPartition::try_from_store(
9,
current_time,
store.clone(),
)
.await?,
store,
op_count,
})
}
fn snapshot_ring_sector_details(
&self,
mismatched_rings: Vec<u32>,
arc_set: &ArcSet,
) -> K2Result<DhtSnapshot> {
let (ring_sector_hashes, disc_boundary) =
self.partition.ring_details(arc_set, mismatched_rings)?;
Ok(DhtSnapshot::RingSectorDetails {
ring_sector_hashes,
disc_boundary,
})
}
async fn snapshot_disc_sectors(
&self,
arc_set: &ArcSet,
) -> K2Result<DhtSnapshot> {
let (disc_sector_top_hashes, disc_boundary) = self
.partition
.disc_sector_hashes(arc_set, self.store.clone())
.await?;
Ok(DhtSnapshot::DiscSectors {
disc_sector_top_hashes,
disc_boundary,
})
}
async fn snapshot_disc_sector_details(
&self,
mismatched_sector_indices: Vec<u32>,
arc_set: &ArcSet,
store: DynOpStore,
) -> K2Result<DhtSnapshot> {
let (disc_sector_hashes, disc_boundary) = self
.partition
.disc_sector_sector_details(
arc_set,
mismatched_sector_indices,
store,
)
.await?;
Ok(DhtSnapshot::DiscSectorDetails {
disc_sector_hashes,
disc_boundary,
})
}
}