use crate::arc_set::ArcSet;
use crate::combine::combine_hashes;
use crate::{SECTOR_SIZE, TimePartition};
use kitsune2_api::{
DhtArc, DynOpStore, K2Error, K2Result, StoredOp, Timestamp,
};
use std::collections::{HashMap, HashSet};
pub struct HashPartition {
size: u32,
sectors: Vec<TimePartition>,
}
impl std::fmt::Debug for HashPartition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HashPartition")
.field("size", &self.size)
.finish()
}
}
pub(crate) type PartialTimeSliceDetails =
HashMap<u32, HashMap<u32, bytes::Bytes>>;
impl HashPartition {
#[tracing::instrument(level = "debug", skip(store))]
pub async fn try_from_store(
time_factor: u8,
current_time: Timestamp,
store: DynOpStore,
) -> K2Result<Self> {
let num_partitions = (u32::MAX / SECTOR_SIZE) + 1;
let mut sectors = Vec::with_capacity(num_partitions as usize);
for i in 0..(num_partitions - 1) {
sectors.push(
TimePartition::try_from_store(
time_factor,
current_time,
DhtArc::Arc(
i * SECTOR_SIZE,
(i + 1).saturating_mul(SECTOR_SIZE) - 1,
),
store.clone(),
)
.await?,
);
}
sectors.push(
TimePartition::try_from_store(
time_factor,
current_time,
DhtArc::Arc((num_partitions - 1) * SECTOR_SIZE, u32::MAX),
store.clone(),
)
.await?,
);
tracing::info!("Allocated [{}] sectors", sectors.len());
Ok(Self {
size: SECTOR_SIZE,
sectors,
})
}
pub fn next_update_at(&self) -> Timestamp {
self.sectors
.first()
.expect("Always at least one sector")
.next_update_at()
}
pub async fn update(
&mut self,
store: DynOpStore,
current_time: Timestamp,
) -> K2Result<()> {
for partition in self.sectors.iter_mut() {
partition.update(current_time, store.clone()).await?;
}
Ok(())
}
pub async fn inform_ops_stored(
&mut self,
store: DynOpStore,
stored_ops: Vec<StoredOp>,
) -> K2Result<()> {
let by_location = stored_ops
.into_iter()
.map(|op| {
let location = op.op_id.loc();
(location / self.size, op)
})
.fold(
HashMap::<u32, Vec<StoredOp>>::new(),
|mut acc, (location, op)| {
acc.entry(location).or_default().push(op);
acc
},
);
for (location, ops) in by_location {
self.sectors[location as usize]
.inform_ops_stored(ops, store.clone())
.await?;
}
Ok(())
}
}
impl HashPartition {
pub(crate) fn dht_arc_for_sector_index(
&self,
sector_index: u32,
) -> K2Result<DhtArc> {
let sector_index = sector_index as usize;
if sector_index >= self.sectors.len() {
return Err(K2Error::other("Sector index out of bounds"));
}
Ok(*self.sectors[sector_index].sector_constraint())
}
pub(crate) fn time_bounds_for_full_slice_index(
&self,
slice_index: u64,
) -> K2Result<(Timestamp, Timestamp)> {
self.sectors[0].time_bounds_for_full_slice_index(slice_index)
}
pub(crate) fn time_bounds_for_partial_slice_index(
&self,
slice_index: u32,
) -> K2Result<(Timestamp, Timestamp)> {
self.sectors[0].time_bounds_for_partial_slice_index(slice_index)
}
pub(crate) async fn disc_top_hash(
&self,
arc_set: &ArcSet,
store: DynOpStore,
) -> K2Result<(bytes::Bytes, Timestamp)> {
let mut combined = bytes::BytesMut::new();
for (sector_index, sector) in self.sectors.iter().enumerate() {
if !arc_set.includes_sector_index(sector_index as u32) {
continue;
}
let hash = sector.full_time_slice_top_hash(store.clone()).await?;
if !hash.is_empty() {
combine_hashes(&mut combined, hash);
}
}
let timestamp = self.sectors[0].full_slice_end_timestamp();
Ok((combined.freeze(), timestamp))
}
pub(crate) fn ring_top_hashes(
&self,
arc_set: &ArcSet,
) -> Vec<bytes::Bytes> {
let mut partials = Vec::with_capacity(arc_set.covered_sector_count());
for (sector_index, sector) in self.sectors.iter().enumerate() {
if !arc_set.includes_sector_index(sector_index as u32) {
continue;
}
partials.push(sector.partial_slice_hashes().peekable());
}
let mut out = Vec::new();
let mut combined = bytes::BytesMut::new();
while partials.get_mut(0).and_then(|p| p.peek()).is_some() {
combined.clear();
for partial in &mut partials {
if let Some(hash) = partial.next()
&& !hash.is_empty()
{
combine_hashes(&mut combined, hash);
if combined.iter().all(|b| *b == 0) {
tracing::warn!(
"Blank combined hash, has the DHT model been informed about the same op(s) more than once?"
);
}
}
}
out.push(combined.clone().freeze());
}
out
}
pub(crate) async fn disc_sector_hashes(
&self,
arc_set: &ArcSet,
store: DynOpStore,
) -> K2Result<(HashMap<u32, bytes::Bytes>, Timestamp)> {
let mut out = HashMap::new();
for (sector_index, sector) in self.sectors.iter().enumerate() {
if !arc_set.includes_sector_index(sector_index as u32) {
continue;
}
let hash = sector.full_time_slice_top_hash(store.clone()).await?;
if !hash.is_empty() {
out.insert(sector_index as u32, hash);
}
}
let timestamp = self.sectors[0].full_slice_end_timestamp();
Ok((out, timestamp))
}
pub(crate) async fn disc_sector_sector_details(
&self,
arc_set: &ArcSet,
sector_indices: Vec<u32>,
store: DynOpStore,
) -> K2Result<(HashMap<u32, HashMap<u64, bytes::Bytes>>, Timestamp)> {
let sectors_indices =
sector_indices.into_iter().collect::<HashSet<_>>();
let mut out = HashMap::new();
for (sector_index, sector) in self.sectors.iter().enumerate() {
if !arc_set.includes_sector_index(sector_index as u32)
|| !sectors_indices.contains(&(sector_index as u32))
{
continue;
}
out.insert(
sector_index as u32,
sector
.full_time_slice_hashes(store.clone())
.await?
.into_iter()
.collect(),
);
}
let timestamp = self.sectors[0].full_slice_end_timestamp();
Ok((out, timestamp))
}
pub(crate) fn ring_details(
&self,
arc_set: &ArcSet,
ring_indices: Vec<u32>,
) -> K2Result<(PartialTimeSliceDetails, Timestamp)> {
let mut out = HashMap::new();
for (sector_index, sector) in self.sectors.iter().enumerate() {
if !arc_set.includes_sector_index(sector_index as u32) {
continue;
}
for ring_index in &ring_indices {
let hash = sector.partial_slice_hash(*ring_index)?;
let entry = out.entry(*ring_index).or_insert_with(HashMap::new);
if !hash.is_empty() {
entry.insert(sector_index as u32, hash);
}
}
}
let timestamp = self.sectors[0].full_slice_end_timestamp();
Ok((out, timestamp))
}
}
#[cfg(test)]
impl HashPartition {
pub fn full_slice_end_timestamp(&self) -> Timestamp {
self.sectors[0].full_slice_end_timestamp()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::UNIT_TIME;
use crate::test::test_store;
use kitsune2_api::{OpId, UNIX_TIMESTAMP};
use kitsune2_core::factories::MemoryOp;
use kitsune2_test_utils::enable_tracing;
use std::time::Duration;
#[tokio::test]
async fn try_from_store() {
enable_tracing();
let store = test_store().await;
let ph = HashPartition::try_from_store(14, Timestamp::now(), store)
.await
.unwrap();
assert_eq!(512, ph.sectors.len());
assert_eq!((u32::MAX / 512) + 1, ph.size);
assert_eq!(
&DhtArc::Arc(0, ph.size - 1),
ph.sectors[0].sector_constraint()
);
assert_eq!(
&DhtArc::Arc(511 * ph.size, u32::MAX),
ph.sectors[511].sector_constraint()
);
}
#[tokio::test]
async fn covers_full_arc() {
enable_tracing();
let store = test_store().await;
let ph = HashPartition::try_from_store(14, UNIX_TIMESTAMP, store)
.await
.unwrap();
let mut start: u32 = 0;
for i in 0..(ph.sectors.len() - 1) {
let end = start.overflowing_add(ph.size).0;
assert_eq!(
DhtArc::Arc(start, end - 1),
*ph.sectors[i].sector_constraint()
);
start = end;
}
assert_eq!(
DhtArc::Arc(start, u32::MAX),
*ph.sectors.last().unwrap().sector_constraint()
);
}
#[tokio::test]
async fn inform_ops_stored_in_full_slices() {
enable_tracing();
let store = test_store().await;
let mut ph =
HashPartition::try_from_store(14, Timestamp::now(), store.clone())
.await
.unwrap();
let op_id_bytes_1 = bytes::Bytes::from_static(&[7, 0, 0, 0]);
let op_id_bytes_2 = bytes::Bytes::from(ph.size.to_le_bytes().to_vec());
ph.inform_ops_stored(
store.clone(),
vec![
StoredOp {
op_id: OpId::from(op_id_bytes_1.clone()),
created_at: UNIX_TIMESTAMP,
},
StoredOp {
op_id: OpId::from(op_id_bytes_2.clone()),
created_at: UNIX_TIMESTAMP
+ ph.sectors[0].full_slice_duration(),
},
],
)
.await
.unwrap();
let count = store
.slice_hash_count(DhtArc::Arc(0, ph.size - 1))
.await
.unwrap();
assert_eq!(1, count);
let hash = store
.retrieve_slice_hash(DhtArc::Arc(0, ph.size - 1), 0)
.await
.unwrap();
assert!(hash.is_some());
assert_eq!(op_id_bytes_1, hash.unwrap());
let count = store
.slice_hash_count(DhtArc::Arc(ph.size, 2 * ph.size - 1))
.await
.unwrap();
assert_eq!(2, count);
let hash = store
.retrieve_slice_hash(DhtArc::Arc(ph.size, 2 * ph.size - 1), 1)
.await
.unwrap();
assert!(hash.is_some());
assert_eq!(op_id_bytes_2, hash.unwrap());
}
#[tokio::test]
async fn inform_ops_stored_in_partial_slices() {
enable_tracing();
let store = test_store().await;
let mut ph =
HashPartition::try_from_store(14, Timestamp::now(), store.clone())
.await
.unwrap();
let op_id_bytes_1 = bytes::Bytes::from_static(&[100, 0, 0, 0]);
let op_id_bytes_2 = bytes::Bytes::from(ph.size.to_le_bytes().to_vec());
ph.inform_ops_stored(
store.clone(),
vec![
StoredOp {
op_id: OpId::from(op_id_bytes_1.clone()),
created_at: ph.sectors[0].full_slice_end_timestamp(),
},
StoredOp {
op_id: OpId::from(op_id_bytes_2.clone()),
created_at: ph.sectors[0].full_slice_end_timestamp()
+ Duration::from_secs((1 << 13) * UNIT_TIME.as_secs()),
},
],
)
.await
.unwrap();
for i in 0..(u32::MAX / ph.size) {
let count = store
.slice_hash_count(DhtArc::Arc(i * ph.size, (i + 1) * ph.size))
.await
.unwrap();
assert_eq!(0, count);
}
let partial_slice = &ph.sectors[0].partials()[0];
assert_eq!(
op_id_bytes_1,
bytes::Bytes::from(partial_slice.hash().to_vec())
);
let partial_slice = &ph.sectors[1].partials()[1];
assert_eq!(
op_id_bytes_2,
bytes::Bytes::from(partial_slice.hash().to_vec())
);
}
#[tokio::test]
async fn next_update_at_consistent() {
enable_tracing();
let store = test_store().await;
let now = Timestamp::now();
let ph = HashPartition::try_from_store(14, now, store.clone())
.await
.unwrap();
let hashes_next_update_at = ph.next_update_at();
assert!(hashes_next_update_at >= now);
for h in ph.sectors {
assert_eq!(hashes_next_update_at, h.next_update_at());
}
}
#[tokio::test]
async fn update_all() {
enable_tracing();
let store = test_store().await;
let now = Timestamp::now();
let mut ph = HashPartition::try_from_store(14, now, store.clone())
.await
.unwrap();
assert_eq!(512, ph.sectors.len());
for h in ph.sectors.iter() {
let (start, _) = match h.sector_constraint() {
DhtArc::Arc(s, e) => (s, e),
_ => panic!("Expected an arc"),
};
store
.process_incoming_ops(vec![
MemoryOp::new(
now,
(start + 1).to_le_bytes().as_slice().to_vec(),
)
.into(),
])
.await
.unwrap();
}
for h in &ph.sectors {
for ps in h.partials() {
assert!(ps.hash().is_empty())
}
}
ph.update(store, now + UNIT_TIME).await.unwrap();
for h in &ph.sectors {
assert_eq!(
1,
h.partials()
.iter()
.filter(|ps| !ps.hash().is_empty())
.count()
);
}
}
}