use crate::combine;
use crate::constant::UNIT_TIME;
use kitsune2_api::{
DhtArc, DynOpStore, K2Error, K2Result, StoredOp, Timestamp, UNIX_TIMESTAMP,
};
use std::time::Duration;
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct TimePartition {
factor: u8,
full_slices: u64,
partial_slices: Vec<PartialSlice>,
full_slice_duration: Duration,
min_recent_time: Duration,
next_update_at: Timestamp,
sector_constraint: DhtArc,
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct PartialSlice {
start: Timestamp,
size: u8,
hash: bytes::BytesMut,
}
impl PartialSlice {
fn end(&self) -> Timestamp {
self.start
+ Duration::from_secs((1u64 << self.size) * UNIT_TIME.as_secs())
}
#[cfg(test)]
pub(crate) fn hash(&self) -> &[u8] {
&self.hash
}
}
impl TimePartition {
#[tracing::instrument(level = "trace", skip(store))]
pub async fn try_from_store(
factor: u8,
current_time: Timestamp,
sector_constraint: DhtArc,
store: DynOpStore,
) -> K2Result<Self> {
if sector_constraint == DhtArc::Empty {
return Err(K2Error::other("Empty arc constraint is not valid"));
}
let mut pt = Self::new(factor, sector_constraint)?;
pt.full_slices = store.slice_hash_count(sector_constraint).await?;
if pt.full_slices == 0 {
let earliest_timestamp = store
.earliest_timestamp_in_arc(sector_constraint)
.await?
.unwrap_or(current_time);
let index = earliest_timestamp.as_micros() as u128
/ pt.full_slice_duration.as_micros();
pt.full_slices = index.saturating_sub(1) as u64;
tracing::trace!("Corrected full slices to {}", pt.full_slices);
}
let full_slice_end_timestamp = pt.full_slice_end_timestamp();
let recent_time = (current_time - full_slice_end_timestamp).map_err(|_| {
K2Error::other("Failed to calculate recent time, either the clock is wrong or this is a bug")
})?;
if pt.full_slices > 0 && recent_time < pt.min_recent_time {
return Err(K2Error::other(
"Not enough recent time reserved, either the clock is wrong or this is a bug",
));
}
pt.update(current_time, store).await?;
Ok(pt)
}
pub fn next_update_at(&self) -> Timestamp {
self.next_update_at
}
pub async fn update(
&mut self,
current_time: Timestamp,
store: DynOpStore,
) -> K2Result<()> {
self.update_full_slice_hashes(current_time, store.clone())
.await?;
let earliest_timestamp = store
.earliest_timestamp_in_arc(self.sector_constraint)
.await?;
self.update_partials(earliest_timestamp, current_time, store.clone())
.await?;
if let Some(last_partial) = self.partial_slices.last() {
self.next_update_at = last_partial.end() + UNIT_TIME;
}
Ok(())
}
pub(crate) async fn inform_ops_stored(
&mut self,
stored_ops: Vec<StoredOp>,
store: DynOpStore,
) -> K2Result<()> {
let full_slice_end = self.full_slice_end_timestamp();
for op in stored_ops {
if op.created_at < full_slice_end {
tracing::info!(
"Historical update detected. Seeing many of these places load on our system, but it is expected if we've been offline or a network partition has been resolved."
);
let slice_index = op.created_at.as_micros()
/ (self.full_slice_duration.as_micros() as i64);
let current_hash = store
.retrieve_slice_hash(
self.sector_constraint,
slice_index as u64,
)
.await?;
match current_hash {
Some(hash) => {
let mut hash = bytes::BytesMut::from(hash);
combine::combine_hashes(&mut hash, op.op_id.0.0);
store
.store_slice_hash(
self.sector_constraint,
slice_index as u64,
hash.freeze(),
)
.await?;
}
None => {
store
.store_slice_hash(
self.sector_constraint,
slice_index as u64,
op.op_id.0.0,
)
.await?;
}
}
} else {
let end_of_partials = match self
.partial_slices
.last()
.map(|last| last.end())
{
Some(end) => end,
None => {
tracing::warn!(
"No partial slices yet, can't update partials. This is likely a configuration or clock issue."
);
continue;
}
};
if op.created_at >= end_of_partials {
continue;
}
for partial in self.partial_slices.iter_mut().rev() {
if op.created_at >= partial.start {
combine::combine_hashes(
&mut partial.hash,
op.op_id.0.0,
);
break;
}
}
}
}
Ok(())
}
pub(crate) fn time_bounds_for_full_slice_index(
&self,
slice_index: u64,
) -> K2Result<(Timestamp, Timestamp)> {
if slice_index > self.full_slices {
return Err(K2Error::other(
"Requested slice index is beyond the current full slices",
));
}
let start = UNIX_TIMESTAMP
+ Duration::from_secs(
slice_index * self.full_slice_duration.as_secs(),
);
let end = start + self.full_slice_duration;
Ok((start, end))
}
pub(crate) fn time_bounds_for_partial_slice_index(
&self,
slice_index: u32,
) -> K2Result<(Timestamp, Timestamp)> {
let slice_index = slice_index as usize;
if slice_index > self.partial_slices.len() {
return Err(K2Error::other(
"Requested slice index is beyond the current partial slices",
));
}
let partial = &self.partial_slices[slice_index];
Ok((partial.start, partial.end()))
}
}
impl TimePartition {
pub async fn full_time_slice_top_hash(
&self,
store: DynOpStore,
) -> K2Result<bytes::Bytes> {
let hashes =
store.retrieve_slice_hashes(self.sector_constraint).await?;
Ok(
combine::combine_op_hashes(
hashes.into_iter().map(|(_, hash)| hash),
)
.freeze(),
)
}
pub fn partial_slice_hashes(
&self,
) -> impl Iterator<Item = bytes::Bytes> + use<'_> {
self.partial_slices
.iter()
.map(|partial| partial.hash.clone().freeze())
}
pub async fn full_time_slice_hashes(
&self,
store: DynOpStore,
) -> K2Result<Vec<(u64, bytes::Bytes)>> {
store.retrieve_slice_hashes(self.sector_constraint).await
}
pub fn partial_slice_hash(
&self,
slice_index: u32,
) -> K2Result<bytes::Bytes> {
Ok(self
.partial_slices
.get(slice_index as usize)
.ok_or_else(|| {
K2Error::other(
"Requested slice index is beyond the current partial slices",
)
})?
.hash
.clone()
.freeze())
}
}
impl TimePartition {
fn new(factor: u8, sector_constraint: DhtArc) -> K2Result<Self> {
Ok(Self {
factor,
full_slices: 0,
partial_slices: Vec::new(),
full_slice_duration: Duration::from_secs(
(1u64 << factor) * UNIT_TIME.as_secs(),
),
min_recent_time: residual_duration_for_factor(factor - 1)?,
next_update_at: Timestamp::from_micros(0),
sector_constraint,
})
}
pub(crate) fn full_slice_end_timestamp(&self) -> Timestamp {
let full_slices_duration = Duration::from_secs(
self.full_slices * self.full_slice_duration.as_secs(),
);
UNIX_TIMESTAMP + full_slices_duration
}
pub(crate) fn sector_constraint(&self) -> &DhtArc {
&self.sector_constraint
}
fn layout_full_slices(&self, current_time: Timestamp) -> K2Result<u64> {
let full_slices_end_timestamp = self.full_slice_end_timestamp();
let recent_time =
(current_time - full_slices_end_timestamp).map_err(|_| {
K2Error::other(
"Current time is before the complete time slice boundary",
)
})?;
let new_full_slices_count = if recent_time > self.min_recent_time {
(recent_time - self.min_recent_time).as_secs()
/ (self.full_slice_duration.as_secs())
} else {
0
};
Ok(new_full_slices_count)
}
async fn update_full_slice_hashes(
&mut self,
current_time: Timestamp,
store: DynOpStore,
) -> K2Result<()> {
let new_full_slices_count = self.layout_full_slices(current_time)?;
let mut full_slices_end_timestamp = self.full_slice_end_timestamp();
for _ in 0..new_full_slices_count {
let op_hashes = store
.retrieve_op_hashes_in_time_slice(
self.sector_constraint,
full_slices_end_timestamp,
full_slices_end_timestamp + self.full_slice_duration,
)
.await?
.0;
let hash = combine::combine_op_hashes(op_hashes);
if !hash.is_empty() {
store
.store_slice_hash(
self.sector_constraint,
self.full_slices,
hash.freeze(),
)
.await?;
}
self.full_slices += 1;
full_slices_end_timestamp += self.full_slice_duration;
}
Ok(())
}
fn layout_partials(
&self,
mut start_at: Timestamp,
current_time: Timestamp,
) -> K2Result<Vec<(Timestamp, u8)>> {
let mut recent_time = (current_time - start_at).map_err(|_| {
K2Error::other("Failed to calculate recent time for partials, either the clock is wrong or this is a bug")
})?;
let mut partials = Vec::new();
for i in (0..self.factor).rev() {
let slice_size =
Duration::from_secs((1u64 << i) * UNIT_TIME.as_secs());
let slice_count = if recent_time
> residual_duration_for_factor(i)? + slice_size
{
2
} else if recent_time > slice_size {
1
} else {
continue;
};
for _ in 0..slice_count {
partials.push((start_at, i));
start_at += slice_size;
recent_time -= slice_size;
}
}
Ok(partials)
}
async fn update_partials(
&mut self,
earliest_timestamp: Option<Timestamp>,
current_time: Timestamp,
store: DynOpStore,
) -> K2Result<()> {
let full_slices_end_timestamp = self.full_slice_end_timestamp();
let new_partials =
self.layout_partials(full_slices_end_timestamp, current_time)?;
let old_partials = std::mem::take(&mut self.partial_slices);
for (start, size) in new_partials.into_iter() {
let maybe_old_hash = old_partials.iter().find_map(|b| {
if b.start == start && b.size == size {
Some(b.hash.clone())
} else {
None
}
});
let hash = match maybe_old_hash {
Some(h) => h,
None => {
let end = start
+ Duration::from_secs(
(1u64 << size) * UNIT_TIME.as_secs(),
);
match earliest_timestamp {
Some(t) if end >= t => combine::combine_op_hashes(
store
.retrieve_op_hashes_in_time_slice(
self.sector_constraint,
start,
end,
)
.await?
.0,
),
_ => {
bytes::BytesMut::new()
}
}
}
};
self.partial_slices.push(PartialSlice { start, size, hash })
}
Ok(())
}
#[cfg(test)]
pub(crate) fn full_slice_duration(&self) -> Duration {
self.full_slice_duration
}
#[cfg(test)]
pub(crate) fn partials(&self) -> &[PartialSlice] {
&self.partial_slices
}
}
fn residual_duration_for_factor(factor: u8) -> K2Result<Duration> {
if factor >= 54 {
return Err(K2Error::other(
"Time partitioning factor must be less than 54",
));
}
let mut sum = 1;
for i in 1..=factor {
sum |= 1u64 << i;
}
Ok(Duration::from_secs(sum * UNIT_TIME.as_secs()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::test_store;
use kitsune2_api::OpId;
use kitsune2_core::factories::MemoryOp;
use kitsune2_test_utils::enable_tracing;
#[test]
fn residual() {
assert_eq!(UNIT_TIME, residual_duration_for_factor(0).unwrap());
assert_eq!(
(2 + 1) * UNIT_TIME,
residual_duration_for_factor(1).unwrap()
);
assert_eq!(
(4 + 2 + 1) * UNIT_TIME,
residual_duration_for_factor(2).unwrap()
);
assert_eq!(
(32 + 16 + 8 + 4 + 2 + 1) * UNIT_TIME,
residual_duration_for_factor(5).unwrap()
);
let mask = 0b1111111111000000u16;
assert_eq!(
Duration::from_secs(!((mask as u64) << 48) * UNIT_TIME.as_secs()),
residual_duration_for_factor(53).unwrap()
);
}
#[should_panic(expected = "Time partitioning factor must be less than 54")]
#[test]
fn max_residual() {
residual_duration_for_factor(54)
.map_err(|e| e.to_string())
.unwrap();
}
#[test]
fn new() {
let factor = 4;
let pt = TimePartition::new(factor, DhtArc::FULL).unwrap();
assert_eq!((8 + 4 + 2 + 1) * UNIT_TIME, pt.min_recent_time);
}
#[tokio::test]
async fn from_store() {
let factor = 4;
let store = test_store().await;
let pt = TimePartition::try_from_store(
factor,
UNIX_TIMESTAMP,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(0, pt.full_slices);
assert!(pt.partial_slices.is_empty());
}
#[tokio::test]
async fn one_partial_slice() {
let current_time =
UNIX_TIMESTAMP + Duration::from_secs(UNIT_TIME.as_secs() + 1);
let factor = 4;
let store = test_store().await;
let pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(0, pt.full_slices);
assert_eq!(1, pt.partial_slices.len());
assert_eq!(0, pt.partial_slices[0].size);
assert_eq!(UNIX_TIMESTAMP, pt.partial_slices[0].start);
assert_eq!(UNIX_TIMESTAMP + UNIT_TIME, pt.partial_slices[0].end());
assert_eq!(UNIX_TIMESTAMP + 2 * UNIT_TIME, pt.next_update_at);
}
#[tokio::test]
async fn all_single_partial_slices() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
min_recent_time(factor).as_secs() + 1,
);
let store = test_store().await;
let pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(0, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
validate_partial_slices(&pt);
}
#[tokio::test]
async fn one_double_others_single_partial_slices() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
min_recent_time(factor).as_secs() +
(1u64 << (factor - 1)) * UNIT_TIME.as_secs()
+ 1,
);
let store = test_store().await;
let pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(factor as usize + 1, pt.partial_slices.len());
assert_eq!(pt.partial_slices[0].size, factor - 1);
assert_eq!(pt.partial_slices[0].size, pt.partial_slices[1].size);
validate_partial_slices(&pt);
}
#[tokio::test]
async fn all_double_slices() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
2 * min_recent_time(factor).as_secs() + 1,
);
let store = test_store().await;
let pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(factor as usize * 2, pt.partial_slices.len());
validate_partial_slices(&pt);
}
#[tokio::test]
async fn hashes_are_combined_for_partial_slices() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
min_recent_time(factor).as_secs() + 1,
);
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new((current_time - UNIT_TIME).unwrap(), vec![7; 32])
.into(),
MemoryOp::new(
(current_time - UNIT_TIME).unwrap(),
vec![23; 32],
)
.into(),
])
.await
.unwrap();
let pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(factor as usize, pt.partial_slices.len());
let last_partial = pt.partial_slices.last().unwrap();
assert_eq!(vec![7 ^ 23; 32], last_partial.hash);
validate_partial_slices(&pt);
}
#[tokio::test]
async fn full_slices_with_single_slice_partials() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
2 * full_slice_duration(factor).as_secs() +
min_recent_time(factor)
.as_secs()
+ 1,
);
let store = test_store().await;
let arc_constraint = DhtArc::Arc(0, 2);
store
.store_slice_hash(arc_constraint, 0, vec![1; 64].into())
.await
.unwrap();
store
.store_slice_hash(arc_constraint, 1, vec![1; 64].into())
.await
.unwrap();
let pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store,
)
.await
.unwrap();
assert_eq!(2, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
validate_partial_slices(&pt);
}
#[tokio::test]
async fn missing_full_slices_combines_hashes() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
full_slice_duration(factor).as_secs() +
min_recent_time(factor)
.as_secs()
+ 1,
);
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new(UNIX_TIMESTAMP, vec![7; 32]).into(),
MemoryOp::new(UNIX_TIMESTAMP, vec![23; 32]).into(),
])
.await
.unwrap();
let arc_constraint = DhtArc::Arc(0, 2);
let pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
assert_eq!(1, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
assert_eq!(1, store.slice_hash_count(arc_constraint).await.unwrap());
let full_slice_hash = store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap();
assert_eq!(vec![7 ^ 23; 32], full_slice_hash);
validate_partial_slices(&pt);
}
#[tokio::test]
async fn compute_hashes_for_full_slices_and_partials() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
full_slice_duration(factor).as_secs() +
min_recent_time(factor)
.as_secs()
+ 1,
);
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new(UNIX_TIMESTAMP, vec![7; 32]).into(),
MemoryOp::new(UNIX_TIMESTAMP, vec![23; 32]).into(),
MemoryOp::new(
(current_time - UNIT_TIME).unwrap(),
vec![11; 32],
)
.into(),
MemoryOp::new(
(current_time - UNIT_TIME).unwrap(),
vec![29; 32],
)
.into(),
])
.await
.unwrap();
let arc_constraint = DhtArc::Arc(0, 2);
let pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
assert_eq!(1, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
assert_eq!(1, store.slice_hash_count(arc_constraint).await.unwrap());
let full_slice_hash = store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap();
assert_eq!(vec![7 ^ 23; 32], full_slice_hash);
let last_partial = pt.partial_slices.last().unwrap();
assert_eq!(vec![11 ^ 29; 32], last_partial.hash);
validate_partial_slices(&pt);
}
#[tokio::test]
async fn update_is_idempotent() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
full_slice_duration(factor).as_secs() +
min_recent_time(factor)
.as_secs()
+ 1,
);
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new(UNIX_TIMESTAMP, vec![7; 32]).into(),
MemoryOp::new(
(current_time - UNIT_TIME).unwrap(),
vec![11; 32],
)
.into(),
])
.await
.unwrap();
let mut pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store.clone(),
)
.await
.unwrap();
let pt_original = pt.clone();
for _ in 1..10 {
let call_at =
current_time + Duration::from_secs(UNIT_TIME.as_secs() / 100);
assert!(call_at < pt.next_update_at());
pt.update(call_at, store.clone()).await.unwrap();
assert_eq!(pt_original, pt);
}
}
#[tokio::test]
async fn update_allocate_new_partial() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
full_slice_duration(factor).as_secs() +
min_recent_time(factor)
.as_secs()
+ 1,
);
let store = test_store().await;
let arc_constraint = DhtArc::Arc(0, 2);
store
.store_slice_hash(arc_constraint, 0, vec![1; 64].into())
.await
.unwrap();
store
.process_incoming_ops(vec![
MemoryOp::new((current_time - UNIT_TIME).unwrap(), vec![7; 32])
.into(),
MemoryOp::new(
(current_time - UNIT_TIME).unwrap(),
vec![29; 32],
)
.into(),
])
.await
.unwrap();
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
assert_eq!(factor as usize, pt.partial_slices.len());
let last_partial = pt.partial_slices.last().unwrap();
assert_eq!(vec![7 ^ 29; 32], last_partial.hash);
store
.process_incoming_ops(vec![
MemoryOp::new(current_time, vec![13; 32]).into(),
])
.await
.unwrap();
pt.update(current_time + UNIT_TIME, store.clone())
.await
.unwrap();
assert_eq!(factor as usize + 1, pt.partial_slices.len());
let second_last_partial = pt.partial_slices.iter().nth_back(1).unwrap();
assert_eq!(vec![7 ^ 29; 32], second_last_partial.hash);
let last_partial = pt.partial_slices.last().unwrap();
assert_eq!(vec![13; 32], last_partial.hash);
}
#[tokio::test]
async fn update_allocate_new_complete() {
let factor = 7;
let current_times = UNIX_TIMESTAMP
+ Duration::from_secs(
full_slice_duration(factor).as_secs() +
min_recent_time(factor)
.as_secs()
+ 1,
);
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new(UNIX_TIMESTAMP, vec![7; 32]).into(),
MemoryOp::new(UNIX_TIMESTAMP, vec![23; 32]).into(),
])
.await
.unwrap();
let arc_constraint = DhtArc::Arc(0, 2);
let mut pt = TimePartition::try_from_store(
factor,
current_times,
arc_constraint,
store.clone(),
)
.await
.unwrap();
assert_eq!(1, pt.full_slices);
assert_eq!(
vec![7 ^ 23; 32],
store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap()
);
store
.process_incoming_ops(vec![
MemoryOp::new(
pt.full_slice_end_timestamp(), vec![13; 32],
)
.into(),
])
.await
.unwrap();
pt.update(
UNIX_TIMESTAMP
+ 2 * full_slice_duration(factor)
+ min_recent_time(factor)
+ Duration::from_secs(1),
store.clone(),
)
.await
.unwrap();
assert_eq!(2, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
assert_eq!(2, store.slice_hash_count(arc_constraint).await.unwrap());
assert_eq!(
vec![7 ^ 23; 32],
store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap()
);
assert_eq!(
vec![13; 32],
store
.retrieve_slice_hash(arc_constraint, 1)
.await
.unwrap()
.unwrap()
);
}
#[tokio::test]
async fn update_allocate_multiple_new_complete() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(min_recent_time(factor).as_secs() + 1);
let store = test_store().await;
let arc_constraint = DhtArc::Arc(0, 2);
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
assert_eq!(0, pt.full_slices);
store
.process_incoming_ops(vec![
MemoryOp::new(UNIX_TIMESTAMP, vec![7; 32]).into(),
MemoryOp::new(UNIX_TIMESTAMP, vec![23; 32]).into(),
MemoryOp::new(
UNIX_TIMESTAMP + pt.full_slice_duration,
vec![11; 32],
)
.into(),
MemoryOp::new(
UNIX_TIMESTAMP + pt.full_slice_duration,
vec![37; 32],
)
.into(),
])
.await
.unwrap();
pt.update(current_time + (2 * pt.full_slice_duration), store.clone())
.await
.unwrap();
assert_eq!(2, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
assert_eq!(2, store.slice_hash_count(arc_constraint).await.unwrap());
assert_eq!(
vec![7 ^ 23; 32],
store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap()
);
assert_eq!(
vec![11 ^ 37; 32],
store
.retrieve_slice_hash(arc_constraint, 1)
.await
.unwrap()
.unwrap()
);
}
#[tokio::test]
async fn compression_all_empty_slices_at_current_time() {
enable_tracing();
let factor = 7;
let current_time = Timestamp::now();
let store = test_store().await;
let arc_constraint = DhtArc::FULL;
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
let initial_full_slices_count = pt.full_slices;
assert_eq!(
(current_time.as_micros() as u128 - pt.min_recent_time.as_micros())
/ (pt.full_slice_duration.as_micros()),
pt.full_slices as u128
);
let slice_hash_count =
store.slice_hash_count(arc_constraint).await.unwrap();
assert_eq!(0, slice_hash_count);
let some_slice_hash = store
.retrieve_slice_hash(arc_constraint, 5203984823)
.await
.unwrap();
assert!(some_slice_hash.is_none());
store
.process_incoming_ops(vec![
MemoryOp::new(pt.full_slice_end_timestamp(), vec![7; 32])
.into(),
])
.await
.unwrap();
pt.update(Timestamp::now() + pt.full_slice_duration, store.clone())
.await
.unwrap();
assert!(store.slice_hash_count(arc_constraint).await.unwrap() > 15_000);
assert_eq!(
store.slice_hash_count(arc_constraint).await.unwrap(),
initial_full_slices_count + 1
);
}
#[tokio::test]
async fn inform_ops_stored_for_empty_full_slice() {
enable_tracing();
let factor = 14;
let current_time = Timestamp::now();
let store = test_store().await;
let arc_constraint = DhtArc::Arc(0, 32);
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
let initial_hash =
store.retrieve_slice_hash(arc_constraint, 0).await.unwrap();
assert!(initial_hash.is_none());
pt.inform_ops_stored(
vec![StoredOp {
op_id: OpId::from(bytes::Bytes::copy_from_slice(&[
11, 0, 0, 0,
])),
created_at: UNIX_TIMESTAMP,
}],
store.clone(),
)
.await
.unwrap();
let updated_hash = store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap();
assert_eq!(vec![11, 0, 0, 0], updated_hash);
}
#[tokio::test]
async fn inform_ops_stored_for_existing_full_slice() {
enable_tracing();
let factor = 14;
let current_time = Timestamp::now();
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new(UNIX_TIMESTAMP, vec![7, 0, 0, 0]).into(),
])
.await
.unwrap();
let arc_constraint = DhtArc::Arc(0, 32);
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
let initial_hash = store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap();
let mut expected = vec![7];
expected.resize(32, 0);
assert_eq!(expected, initial_hash);
let mut inner_op_id = vec![23];
inner_op_id.resize(32, 0);
pt.inform_ops_stored(
vec![StoredOp {
op_id: OpId::from(bytes::Bytes::from(inner_op_id)),
created_at: UNIX_TIMESTAMP,
}],
store.clone(),
)
.await
.unwrap();
let updated_hash = store
.retrieve_slice_hash(arc_constraint, 0)
.await
.unwrap()
.unwrap();
let mut expected = vec![7 ^ 23];
expected.resize(32, 0);
assert_eq!(expected, updated_hash);
}
#[tokio::test]
async fn inform_ops_stored_for_empty_partial_slice() {
enable_tracing();
let factor = 14;
let current_time =
UNIX_TIMESTAMP + min_recent_time(factor) + Duration::from_secs(3);
let store = test_store().await;
let arc_constraint = DhtArc::Arc(0, 32);
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
assert!(pt.partial_slices.iter().all(|slice| slice.hash.is_empty()));
pt.inform_ops_stored(
vec![
StoredOp {
op_id: OpId::from(bytes::Bytes::copy_from_slice(&[
11, 0, 0, 0,
])),
created_at: UNIX_TIMESTAMP,
},
StoredOp {
op_id: OpId::from(bytes::Bytes::copy_from_slice(&[
29, 0, 0, 0,
])),
created_at: (current_time - Duration::from_secs(5))
.unwrap(),
},
],
store.clone(),
)
.await
.unwrap();
assert_eq!(vec![11, 0, 0, 0], pt.partial_slices.first().unwrap().hash);
assert_eq!(vec![29, 0, 0, 0], pt.partial_slices.last().unwrap().hash);
}
#[tokio::test]
async fn inform_ops_stored_for_existing_partial_slice() {
enable_tracing();
let factor = 14;
let current_time =
UNIX_TIMESTAMP + min_recent_time(factor) + Duration::from_secs(3);
let store = test_store().await;
store
.process_incoming_ops(vec![
MemoryOp::new(
UNIX_TIMESTAMP + Duration::from_secs(10),
vec![7, 0, 0, 0],
)
.into(),
MemoryOp::new(
(current_time - Duration::from_secs(30)).unwrap(),
vec![31, 0, 0, 0],
)
.into(),
])
.await
.unwrap();
let arc_constraint = DhtArc::Arc(0, 32);
let mut pt = TimePartition::try_from_store(
factor,
current_time,
arc_constraint,
store.clone(),
)
.await
.unwrap();
let mut expected = vec![7];
expected.resize(32, 0);
assert_eq!(expected, pt.partial_slices.first().unwrap().hash);
let mut expected = vec![31];
expected.resize(32, 0);
assert_eq!(expected, pt.partial_slices.last().unwrap().hash);
let mut inner_op_id_1 = vec![11];
inner_op_id_1.resize(32, 0);
let mut inner_op_id_2 = vec![29];
inner_op_id_2.resize(32, 0);
pt.inform_ops_stored(
vec![
StoredOp {
op_id: OpId::from(bytes::Bytes::from(inner_op_id_1)),
created_at: UNIX_TIMESTAMP,
},
StoredOp {
op_id: OpId::from(bytes::Bytes::from(inner_op_id_2)),
created_at: (current_time - Duration::from_secs(5))
.unwrap(),
},
],
store.clone(),
)
.await
.unwrap();
let mut expected = vec![7 ^ 11];
expected.resize(32, 0);
assert_eq!(expected, pt.partial_slices.first().unwrap().hash);
let mut expected = vec![31 ^ 29];
expected.resize(32, 0);
assert_eq!(expected, pt.partial_slices.last().unwrap().hash);
}
#[tokio::test]
async fn access_out_of_bounds_partial_slice() {
let factor = 7;
let current_time = UNIX_TIMESTAMP
+ Duration::from_secs(
min_recent_time(factor).as_secs() + 1,
);
let store = test_store().await;
let pt = TimePartition::try_from_store(
factor,
current_time,
DhtArc::FULL,
store,
)
.await
.unwrap();
assert_eq!(0, pt.full_slices);
assert_eq!(factor as usize, pt.partial_slices.len());
validate_partial_slices(&pt);
assert!(pt.partial_slice_hash(0).is_ok());
assert!(
pt.partial_slice_hash(pt.partial_slices.len() as u32)
.is_err()
);
assert!(
pt.partial_slice_hash(pt.partial_slices.len() as u32 + 1)
.is_err()
);
}
fn validate_partial_slices(pt: &TimePartition) {
let mut start_at = UNIX_TIMESTAMP
+ Duration::from_secs(
pt.full_slices * full_slice_duration(pt.factor).as_secs(),
);
for (i, slice) in pt.partial_slices.iter().enumerate() {
if i > 0 {
assert!(
pt.partial_slices[i - 1].size >= slice.size,
"factor must decrease"
);
}
if i > 1 {
assert!(
pt.partial_slices[i - 2].size
!= pt.partial_slices[i - 1].size
|| pt.partial_slices[i - 1].size != slice.size,
"no more than two slices of the same size are permitted"
);
}
assert_eq!(start_at, slice.start);
start_at +=
Duration::from_secs((1u64 << slice.size) * UNIT_TIME.as_secs());
}
}
fn min_recent_time(factor: u8) -> Duration {
TimePartition::new(factor, DhtArc::FULL)
.unwrap()
.min_recent_time
}
fn full_slice_duration(factor: u8) -> Duration {
TimePartition::new(factor, DhtArc::FULL)
.unwrap()
.full_slice_duration
}
}