use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use anybytes::Bytes;
use crate::blob::encodings::simplearchive::SimpleArchive;
use crate::blob::encodings::UnknownBlob;
use crate::capability::policy::resource_collection;
use crate::inline::encodings::hash::Handle;
use crate::inline::Inline;
use crate::patch::{Blake3Merkle, Entry, IdentitySchema, PATCH};
use crate::prelude::{find, pattern};
use crate::repo::{BlobStoreGet, BlobStoreList, CapabilityProofRead, StoreChanges, StoreSnapshot};
use crate::trible::TribleSet;
use super::covered::RecordDelta;
use super::descriptor::validate_proof_evidence;
use super::{CollectionHandle, CollectionRead, CollectionRecord, CollectionRecordSelector};
type Raw = [u8; 32];
pub type HeldBlobs = PATCH<32, IdentitySchema, (), Blake3Merkle>;
mod pair_key {
crate::key_segmentation!(Segments, 64, [32, 32]);
crate::key_schema!(Schema, Segments, 64, [0, 1]);
}
type Pairs = PATCH<64, pair_key::Schema, ()>;
type Handles = PATCH<32, IdentitySchema, ()>;
type HeldSets = PATCH<32, IdentitySchema, HeldBlobs>;
fn pair(first: &Raw, second: &Raw) -> [u8; 64] {
let mut key = [0u8; 64];
key[..32].copy_from_slice(first);
key[32..].copy_from_slice(second);
key
}
fn split(key: &[u8; 64]) -> (Raw, Raw) {
let mut first = [0u8; 32];
let mut second = [0u8; 32];
first.copy_from_slice(&key[..32]);
second.copy_from_slice(&key[32..]);
(first, second)
}
fn related(pairs: &Pairs, first: &Raw) -> Vec<Raw> {
let mut seconds = Vec::new();
pairs.infixes(first, |second: &Raw| seconds.push(*second));
seconds
}
fn take_related(pairs: &mut Pairs, first: &Raw) -> Vec<Raw> {
let seconds = related(pairs, first);
for second in &seconds {
pairs.remove(&pair(first, second));
}
seconds
}
pub trait HeldRead {
fn held(&self, collection: CollectionHandle) -> Option<HeldBlobs>;
fn held_generation(&self) -> u64;
}
pub trait HeldStore {
fn track_held(&mut self, collections: impl IntoIterator<Item = CollectionHandle>);
fn note_held(&mut self, collection: CollectionHandle, handle: Inline<Handle<UnknownBlob>>);
}
pub trait HeldSource:
StoreSnapshot + RecordDelta + CollectionRead + BlobStoreGet + BlobStoreList + CapabilityProofRead
{
}
impl<T> HeldSource for T where
T: StoreSnapshot
+ RecordDelta
+ CollectionRead
+ BlobStoreGet
+ BlobStoreList
+ CapabilityProofRead
{
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HeldWalkConfig {
pub interval: Duration,
pub threads: usize,
}
impl Default for HeldWalkConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(30 * 60),
threads: 4,
}
}
}
static WALKER_THREADS: AtomicUsize = AtomicUsize::new(0);
static SPAWNED_THREADS: AtomicUsize = AtomicUsize::new(0);
pub fn held_walker_threads() -> usize {
WALKER_THREADS.load(Ordering::SeqCst)
}
pub fn held_threads_spawned() -> usize {
SPAWNED_THREADS.load(Ordering::SeqCst)
}
#[derive(Clone, Default)]
pub(crate) struct HeldView {
generation: u64,
sets: HeldSets,
}
impl PartialEq for HeldView {
fn eq(&self, other: &Self) -> bool {
self.generation == other.generation
&& self.sets == other.sets
&& self
.sets
.iter()
.all(|collection| self.sets.get(collection) == other.sets.get(collection))
}
}
impl std::fmt::Debug for HeldView {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HeldView")
.field("generation", &self.generation)
.field(
"sets",
&self
.sets
.iter()
.map(|collection| {
(
*collection,
self.sets.get(collection).map(|held| held.len()),
)
})
.collect::<Vec<_>>(),
)
.finish()
}
}
impl HeldView {
pub(crate) fn held(&self, collection: CollectionHandle) -> Option<HeldBlobs> {
self.sets.get(&collection.raw).cloned()
}
pub(crate) fn generation(&self) -> u64 {
self.generation
}
}
#[derive(Clone, Debug, Default)]
struct Walker {
requested: Handles,
threads: usize,
}
#[derive(Clone)]
struct State<T> {
tracked: Handles,
held: HeldSets,
warming: Handles,
seeds: Pairs,
routes: Pairs,
deferred: Pairs,
scanned: Handles,
edges: Pairs,
pending: Pairs,
ready: Pairs,
unrouted: Handles,
proofs_stale: bool,
proofs_retry: bool,
candidates: Pairs,
fed: Option<T>,
generation: u64,
epoch: u64,
view: Arc<HeldView>,
walker: Option<Walker>,
}
pub(crate) struct HeldIndex<T> {
state: Mutex<State<T>>,
wake: Condvar,
epoch: AtomicU64,
#[cfg(test)]
walk_taken: Mutex<Option<Box<dyn FnOnce() + Send>>>,
}
impl<T> Default for HeldIndex<T> {
fn default() -> Self {
Self {
state: Mutex::new(State {
tracked: Handles::new(),
held: HeldSets::new(),
warming: Handles::new(),
seeds: Pairs::new(),
routes: Pairs::new(),
deferred: Pairs::new(),
scanned: Handles::new(),
edges: Pairs::new(),
pending: Pairs::new(),
ready: Pairs::new(),
unrouted: Handles::new(),
proofs_stale: false,
proofs_retry: false,
candidates: Pairs::new(),
fed: None,
generation: 0,
epoch: 0,
view: Arc::new(HeldView::default()),
walker: None,
}),
wake: Condvar::new(),
epoch: AtomicU64::new(0),
#[cfg(test)]
walk_taken: Mutex::new(None),
}
}
}
impl<T: Clone> HeldIndex<T> {
pub(crate) fn detached_clone(&self) -> Self {
let mut state = self.lock().clone();
state.walker = None;
state.release_warming();
let epoch = state.epoch;
Self {
state: Mutex::new(state),
wake: Condvar::new(),
epoch: AtomicU64::new(epoch),
#[cfg(test)]
walk_taken: Mutex::new(None),
}
}
}
impl<T> HeldIndex<T> {
fn lock(&self) -> MutexGuard<'_, State<T>> {
self.state.lock().expect("held-blob index is not poisoned")
}
pub(crate) fn track(&self, collections: impl IntoIterator<Item = CollectionHandle>) {
let mut state = self.lock();
for collection in collections {
state.tracked.insert(&Entry::new(&collection.raw));
}
}
pub(crate) fn note(&self, collection: CollectionHandle, handle: Raw) {
let mut state = self.lock();
if state.tracked.get(&collection.raw).is_some() {
state
.candidates
.insert(&Entry::new(&pair(&collection.raw, &handle)));
}
}
fn walk_taken(&self) {
#[cfg(test)]
if let Some(hook) = self.walk_taken.lock().unwrap().take() {
hook();
}
}
fn abandoned(&self, epoch: u64, stop: Option<&AtomicBool>) -> bool {
stop.is_some_and(|stop| stop.load(Ordering::Relaxed))
|| self.epoch.load(Ordering::Relaxed) != epoch
}
}
fn seeds_of(record: &CollectionRecord) -> Option<Vec<Raw>> {
match record {
CollectionRecord::Commit(commit) => Some(
commit
.blob_references()
.iter()
.map(|handle| handle.raw)
.collect(),
),
CollectionRecord::Derive(derive) => Some(
derive
.blob_references()
.iter()
.map(|handle| handle.raw)
.collect(),
),
CollectionRecord::Merge(_) | CollectionRecord::Map(_) => None,
}
}
struct ProofSeeds {
seeds: Vec<(CollectionHandle, Raw)>,
unrouted: Vec<Raw>,
retry: bool,
}
fn proof_seeds<T: HeldSource>(
snapshot: &T,
collections: &BTreeSet<CollectionHandle>,
) -> Result<ProofSeeds, T::ProofsError> {
let mut found = ProofSeeds {
seeds: Vec::new(),
unrouted: Vec::new(),
retry: false,
};
if collections.is_empty() {
return Ok(found);
}
let mut descriptors: BTreeMap<CollectionHandle, Option<TribleSet>> = BTreeMap::new();
for proof in snapshot.proofs()? {
let proof = proof?;
let resource: CollectionHandle = Inline::new(proof.resource().into_bytes());
let mut candidates = BTreeSet::new();
if collections.contains(&resource) {
candidates.insert(resource);
}
if let Ok(facts) = snapshot.get::<TribleSet, SimpleArchive>(resource) {
for (collection,) in find!(
(collection: Inline<Handle<SimpleArchive>>),
pattern!(&facts, [{ _?resource @ resource_collection: ?collection }])
) {
if collections.contains(&collection) {
candidates.insert(collection);
}
}
} else if snapshot.contains_blob(resource).unwrap_or(false) {
found.retry = true;
} else {
found.unrouted.push(resource.raw);
}
for collection in candidates {
let descriptor = descriptors.entry(collection).or_insert_with(|| {
let facts = snapshot.get::<TribleSet, SimpleArchive>(collection).ok();
if facts.is_none() {
if snapshot.contains_blob(collection).unwrap_or(false) {
found.retry = true;
} else {
found.unrouted.push(collection.raw);
}
}
facts
});
let Some(descriptor) = descriptor else {
continue;
};
if validate_proof_evidence(snapshot, collection, descriptor, &proof).is_ok() {
for handle in proof.blob_references() {
found.seeds.push((collection, handle.raw));
}
}
}
}
Ok(found)
}
fn scan<T: HeldSource>(snapshot: &T, handle: &Raw) -> Option<Vec<Raw>> {
let body: Bytes = snapshot
.get::<Bytes, UnknownBlob>(Inline::<Handle<UnknownBlob>>::new(*handle))
.ok()?;
let mut children: Vec<Raw> = body
.as_ref()
.chunks_exact(32)
.map(|chunk| <Raw>::try_from(chunk).expect("32-byte chunk"))
.filter(|word| {
snapshot
.contains_blob(Inline::<Handle<UnknownBlob>>::new(*word))
.unwrap_or(false)
})
.collect();
children.sort_unstable();
children.dedup();
Some(children)
}
impl<T> State<T> {
fn reset(&mut self) {
self.scanned = Handles::new();
self.edges = Pairs::new();
self.pending = Pairs::new();
self.ready = Pairs::new();
self.epoch += 1;
self.held = HeldSets::new();
self.warming = Handles::new();
}
fn release_warming(&mut self) {
for collection in self.warming.iter() {
self.held.remove(collection);
}
self.warming = Handles::new();
}
fn publish_view(&mut self) {
self.generation += 1;
self.view = Arc::new(HeldView {
generation: self.generation,
sets: self.held.clone(),
});
}
fn walk_roots(&self, only: Option<&Handles>) -> Vec<(CollectionHandle, Vec<Raw>)> {
self.held
.iter()
.filter(|collection| only.is_none_or(|only| only.get(collection).is_some()))
.map(|collection| {
let mut roots = related(&self.seeds, collection);
roots.extend(related(&self.routes, collection));
roots.sort_unstable();
roots.dedup();
(Inline::new(*collection), roots)
})
.collect()
}
fn walked(&mut self) {
if std::mem::take(&mut self.proofs_retry) {
self.proofs_stale = true;
}
}
fn record_scan(&mut self, handle: Raw, children: &[Raw]) -> Vec<Raw> {
self.scanned.insert(&Entry::new(&handle));
let mut learned = Vec::new();
for child in children {
let key = pair(&handle, child);
if self.edges.get(&key).is_none() {
self.edges.insert(&Entry::new(&key));
learned.push(*child);
}
}
learned
}
}
impl<T: HeldSource> State<T> {
fn reach(
&mut self,
snapshot: Option<&T>,
collection: CollectionHandle,
roots: impl IntoIterator<Item = Raw>,
) -> bool {
let Some(mut held) = self.held.get(&collection.raw).cloned() else {
return false;
};
let mut changed = false;
let mut entered: HashSet<Raw> = HashSet::new();
let mut unknown: HashSet<Raw> = HashSet::new();
let mut stack: Vec<(Raw, Option<Vec<Raw>>)> =
roots.into_iter().map(|root| (root, None)).collect();
while let Some((handle, visited)) = stack.pop() {
if let Some(children) = visited {
if children.iter().any(|child| unknown.contains(child)) {
unknown.insert(handle);
} else {
held.insert(&Entry::new(&handle));
changed = true;
}
continue;
}
if held.has_prefix(&handle) || !entered.insert(handle) {
continue;
}
let children = if self.scanned.get(&handle).is_some() {
related(&self.edges, &handle)
} else if let Some(snapshot) = snapshot {
match scan(snapshot, &handle) {
Some(children) => {
self.record_scan(handle, &children);
children
}
None => {
if self.seeds.get(&pair(&collection.raw, &handle)).is_some() {
self.pending
.insert(&Entry::new(&pair(&handle, &collection.raw)));
}
unknown.insert(handle);
continue;
}
}
} else {
unknown.insert(handle);
continue;
};
let unheld: Vec<(Raw, Option<Vec<Raw>>)> = children
.iter()
.filter(|child| !held.has_prefix(*child))
.map(|child| (*child, None))
.collect();
stack.push((handle, Some(children)));
stack.extend(unheld);
}
if changed {
self.held.replace(&Entry::with_value(&collection.raw, held));
}
changed
}
fn add_proof_seeds(
&mut self,
found: ProofSeeds,
roots: &mut BTreeMap<CollectionHandle, Vec<Raw>>,
) {
for descriptor in found.unrouted {
self.unrouted.insert(&Entry::new(&descriptor));
}
for (collection, seed) in found.seeds {
let key = pair(&collection.raw, &seed);
if self.tracked.get(&collection.raw).is_none() || self.seeds.get(&key).is_some() {
continue;
}
self.seeds.insert(&Entry::new(&key));
roots.entry(collection).or_default().push(seed);
}
}
}
impl<T: HeldSource> HeldIndex<T> {
pub(crate) fn observe(&self, now: &T) -> Arc<HeldView> {
let mut state = self.lock();
if state.tracked.is_empty() {
state.candidates = Pairs::new();
return state.view.clone();
}
let mut changed = false;
let changes = state
.fed
.as_ref()
.map_or(StoreChanges::ALL, |fed| now.changes_since(fed));
let lost = changes.contains(StoreChanges::BLOBS)
&& state.fed.as_ref().is_some_and(|fed| {
fed.blobs_diff(now)
.flatten()
.any(|info| !now.contains_blob(info.handle).unwrap_or(false))
});
if lost {
state.reset();
self.epoch.store(state.epoch, Ordering::Relaxed);
changed = true;
}
let mut roots: BTreeMap<CollectionHandle, Vec<Raw>> = BTreeMap::new();
for key in std::mem::take(&mut state.ready).iter() {
let (collection, handle) = split(key);
roots
.entry(Inline::new(collection))
.or_default()
.push(handle);
}
if let Some(fed) = state.fed.take() {
if changes.contains(StoreChanges::COLLECTION_RECORDS) {
let State { held, seeds, .. } = &mut *state;
now.for_each_record_since(Some(&fed), &mut |record| {
let collection = record.collection();
if held.get(&collection.raw).is_none() {
return;
}
let Some(named) = seeds_of(record) else {
return;
};
for seed in named {
seeds.insert(&Entry::new(&pair(&collection.raw, &seed)));
roots.entry(collection).or_default().push(seed);
}
});
}
if changes.contains(StoreChanges::BLOBS)
&& !(state.pending.is_empty() && state.unrouted.is_empty())
{
for info in now.blobs_diff(&fed).flatten() {
let raw = info.handle.raw;
for collection in take_related(&mut state.pending, &raw) {
roots.entry(Inline::new(collection)).or_default().push(raw);
}
if state.unrouted.get(&raw).is_some() {
state.unrouted.remove(&raw);
state.proofs_stale = true;
}
}
}
if changes.contains(StoreChanges::CAPABILITY_PROOFS) || state.proofs_stale {
let settled: BTreeSet<CollectionHandle> = state
.held
.iter()
.map(|collection| Inline::new(*collection))
.collect();
match proof_seeds(now, &settled) {
Ok(found) => {
state.proofs_stale = false;
state.proofs_retry = found.retry;
state.add_proof_seeds(found, &mut roots);
}
Err(_) => state.proofs_stale = true,
}
}
}
let fresh: BTreeSet<CollectionHandle> = state
.tracked
.iter()
.filter(|collection| state.held.get(collection).is_none())
.map(|collection| Inline::new(*collection))
.collect();
if !fresh.is_empty() {
let selectors = fresh
.iter()
.map(|collection| CollectionRecordSelector::Foundations(*collection))
.collect();
if let (Ok(records), Ok(proofs)) =
(now.select_records(&selectors), proof_seeds(now, &fresh))
{
let mut seeds: BTreeMap<CollectionHandle, Vec<Raw>> = fresh
.iter()
.map(|collection| (*collection, vec![collection.raw]))
.collect();
for record in records {
if let (Some(list), Some(named)) =
(seeds.get_mut(&record.collection()), seeds_of(&record))
{
list.extend(named);
}
}
for descriptor in proofs.unrouted {
state.unrouted.insert(&Entry::new(&descriptor));
}
state.proofs_retry |= proofs.retry;
for (collection, seed) in proofs.seeds {
seeds.get_mut(&collection).expect("fresh").push(seed);
}
let background = state.walker.is_some();
for (collection, mut list) in seeds {
for seed in &list {
state
.seeds
.insert(&Entry::new(&pair(&collection.raw, seed)));
}
list.extend(related(&state.routes, &collection.raw));
state
.held
.replace(&Entry::with_value(&collection.raw, HeldBlobs::new()));
if background {
state.warming.insert(&Entry::new(&collection.raw));
state.reach(None, collection, list);
} else {
roots.entry(collection).or_default().extend(list);
}
}
changed = true;
if let Some(walker) = &mut state.walker {
for collection in &fresh {
walker.requested.insert(&Entry::new(&collection.raw));
}
self.wake.notify_all();
}
}
}
for (collection, list) in roots {
changed |= state.reach(Some(now), collection, list);
}
let mut candidates: Vec<(CollectionHandle, Raw)> = std::mem::take(&mut state.candidates)
.iter()
.map(|key| {
let (collection, handle) = split(key);
(Inline::new(collection), handle)
})
.collect();
if !state.deferred.is_empty() {
let cold: Vec<Raw> = state
.held
.iter()
.filter(|collection| state.warming.get(collection).is_none())
.copied()
.collect();
for collection in cold {
for handle in take_related(&mut state.deferred, &collection) {
candidates.push((Inline::new(collection), handle));
}
}
}
for (collection, handle) in candidates {
if state.tracked.get(&collection.raw).is_none() {
continue;
}
let settled = state.held.get(&collection.raw);
if settled.is_some_and(|held| held.has_prefix(&handle)) {
continue;
}
if settled.is_none() || state.warming.get(&collection.raw).is_some() {
if now
.contains_blob(Inline::<Handle<UnknownBlob>>::new(handle))
.unwrap_or(false)
{
state
.deferred
.insert(&Entry::new(&pair(&collection.raw, &handle)));
}
continue;
}
if !now
.contains_blob(Inline::<Handle<UnknownBlob>>::new(handle))
.unwrap_or(false)
{
continue;
}
state
.routes
.insert(&Entry::new(&pair(&collection.raw, &handle)));
changed |= state.reach(Some(now), collection, [handle]);
}
state.fed = Some(now.clone());
if changed {
state.publish_view();
}
state.view.clone()
}
fn publish_unreadable(&self, epoch: u64, unreadable: Vec<(CollectionHandle, Raw)>) {
let mut state = self.lock();
if state.epoch != epoch {
return;
}
for (collection, handle) in unreadable {
let Some(held) = state.held.get(&collection.raw) else {
continue;
};
if held.has_prefix(&handle)
|| state.seeds.get(&pair(&collection.raw, &handle)).is_none()
{
continue;
}
let resident = state.fed.as_ref().is_some_and(|fed| {
fed.contains_blob(Inline::<Handle<UnknownBlob>>::new(handle))
.unwrap_or(false)
});
if resident {
state
.ready
.insert(&Entry::new(&pair(&collection.raw, &handle)));
} else {
state
.pending
.insert(&Entry::new(&pair(&handle, &collection.raw)));
}
}
}
fn publish_piece(&self, epoch: u64, piece: Vec<(Raw, Arc<[Raw]>, Vec<CollectionHandle>)>) {
let mut state = self.lock();
if state.epoch != epoch {
return;
}
let mut changed = false;
for (handle, children, reached) in piece {
let learned = state.record_scan(handle, &children);
for collection in reached {
changed |= state.reach(None, collection, [handle]);
}
if learned.is_empty() {
continue;
}
let holders: Vec<CollectionHandle> = state
.held
.iter()
.filter(|collection| {
state
.held
.get(collection)
.is_some_and(|held| held.has_prefix(&handle))
})
.map(|collection| Inline::new(*collection))
.collect();
for collection in holders {
changed |= state.reach(None, collection, learned.iter().copied());
}
}
if changed {
state.publish_view();
}
}
pub(crate) fn walk(
&self,
threads: usize,
only: Option<&Handles>,
stop: Option<&AtomicBool>,
) -> bool {
let (first, mut queue, epoch) = {
let state = self.lock();
let Some(first) = state.fed.clone() else {
return true;
};
(first, state.walk_roots(only), state.epoch)
};
self.walk_taken();
let covered: Vec<CollectionHandle> =
queue.iter().map(|(collection, _)| *collection).collect();
let threads = threads.max(1);
let abandoned = || self.abandoned(epoch, stop);
let mut known: HashMap<Raw, Option<Arc<[Raw]>>> = HashMap::new();
let mut seen: BTreeMap<CollectionHandle, HashSet<Raw>> = BTreeMap::new();
let mut latest: Option<T> = None;
let mut rounds = 0;
loop {
let snapshot = latest.as_ref().unwrap_or(&first);
while let Some(batch) = next_batch(&mut queue) {
if !self.walk_batch(
snapshot, epoch, threads, &abandoned, batch, &mut known, &mut seen,
) {
return false;
}
}
let mut state = self.lock();
if state.epoch != epoch || abandoned() {
return false;
}
if rounds == MAX_REPORT_ROUNDS {
let owing: Vec<CollectionHandle> = covered
.iter()
.copied()
.filter(|collection| {
!related(&state.deferred, &collection.raw).is_empty()
|| (state.warming.get(&collection.raw).is_some()
&& !related(&state.candidates, &collection.raw).is_empty())
})
.collect();
for collection in &covered {
if !owing.contains(collection) {
state.warming.remove(&collection.raw);
}
}
if let Some(walker) = &mut state.walker {
for collection in &owing {
walker.requested.insert(&Entry::new(&collection.raw));
}
self.wake.notify_all();
}
state.walked();
return true;
}
let mut reports = Vec::new();
for collection in &covered {
if state.warming.get(&collection.raw).is_some() {
for handle in take_related(&mut state.candidates, &collection.raw) {
let resident = state.fed.as_ref().is_some_and(|fed| {
fed.contains_blob(Inline::<Handle<UnknownBlob>>::new(handle))
.unwrap_or(false)
});
let key = pair(&collection.raw, &handle);
if resident {
state.deferred.insert(&Entry::new(&key));
} else {
state.candidates.insert(&Entry::new(&key));
}
}
}
let reached = seen.entry(*collection).or_default();
let mut unreached = Vec::new();
for handle in take_related(&mut state.deferred, &collection.raw) {
if !reached.contains(&handle) {
state
.routes
.insert(&Entry::new(&pair(&collection.raw, &handle)));
unreached.push(handle);
}
}
if !unreached.is_empty() {
reports.push((*collection, unreached));
}
}
if reports.is_empty() {
for collection in &covered {
state.warming.remove(&collection.raw);
}
state.walked();
return true;
}
latest = state.fed.clone();
drop(state);
rounds += 1;
known.retain(|handle, children| {
if children.is_none() {
for seen in seen.values_mut() {
seen.remove(handle);
}
}
children.is_some()
});
queue = reports;
}
}
#[allow(clippy::too_many_arguments)]
fn walk_batch(
&self,
snapshot: &T,
epoch: u64,
threads: usize,
abandoned: &(dyn Fn() -> bool + Sync),
mut frontier: Vec<(CollectionHandle, Vec<Raw>)>,
known: &mut HashMap<Raw, Option<Arc<[Raw]>>>,
seen: &mut BTreeMap<CollectionHandle, HashSet<Raw>>,
) -> bool {
let mut reached: HashMap<Raw, Vec<CollectionHandle>> = HashMap::new();
while frontier.iter().any(|(_, nodes)| !nodes.is_empty()) {
if abandoned() {
return false;
}
let mut need: Vec<Raw> = frontier
.iter()
.flat_map(|(_, nodes)| nodes.iter().copied())
.filter(|handle| !known.contains_key(handle))
.collect();
need.sort_unstable();
need.dedup();
let scanned: Vec<(Raw, Option<Arc<[Raw]>>)> =
scan_all(snapshot, &need, threads, abandoned);
if abandoned() {
return false;
}
for (handle, children) in scanned {
known.insert(handle, children);
}
let mut unreadable = Vec::new();
let mut next = Vec::with_capacity(frontier.len());
for (collection, nodes) in frontier {
let seen = seen.entry(collection).or_default();
let mut after = Vec::new();
for handle in nodes {
if !seen.insert(handle) {
continue;
}
match &known[&handle] {
Some(children) => {
reached.entry(handle).or_default().push(collection);
after.extend(
children
.iter()
.copied()
.filter(|child| !seen.contains(child)),
);
}
None => unreadable.push((collection, handle)),
}
}
next.push((collection, after));
}
self.publish_unreadable(epoch, unreadable);
frontier = next;
}
let order = children_first(reached.keys().copied(), known);
for piece in order.chunks(PUBLISH_PIECE) {
let piece = piece
.iter()
.map(|handle| {
let children = known[handle].clone().expect("a reached blob was read");
(
*handle,
children,
reached.remove(handle).unwrap_or_default(),
)
})
.collect();
self.publish_piece(epoch, piece);
}
true
}
}
fn children_first(
nodes: impl IntoIterator<Item = Raw>,
known: &HashMap<Raw, Option<Arc<[Raw]>>>,
) -> Vec<Raw> {
let nodes: HashSet<Raw> = nodes.into_iter().collect();
let mut order = Vec::with_capacity(nodes.len());
let mut entered: HashSet<Raw> = HashSet::new();
for start in &nodes {
let mut stack = vec![(*start, false)];
while let Some((node, exit)) = stack.pop() {
if exit {
order.push(node);
continue;
}
if !entered.insert(node) {
continue;
}
stack.push((node, true));
if let Some(Some(children)) = known.get(&node) {
stack.extend(
children
.iter()
.filter(|child| nodes.contains(*child) && !entered.contains(*child))
.map(|child| (*child, false)),
);
}
}
}
order
}
fn next_batch(
queue: &mut Vec<(CollectionHandle, Vec<Raw>)>,
) -> Option<Vec<(CollectionHandle, Vec<Raw>)>> {
let mut batch = Vec::new();
let mut budget = WALK_BATCH;
while budget > 0 {
let Some((collection, roots)) = queue.last_mut() else {
break;
};
let take = roots.len().min(budget);
let taken = roots.split_off(roots.len() - take);
budget -= take;
batch.push((*collection, taken));
if roots.is_empty() {
queue.pop();
}
}
(!batch.is_empty()).then_some(batch)
}
const WALK_BATCH: usize = 4_096;
const MAX_REPORT_ROUNDS: usize = 16;
const PUBLISH_PIECE: usize = 16_384;
fn scan_all<T: HeldSource>(
snapshot: &T,
handles: &[Raw],
threads: usize,
abandoned: &(dyn Fn() -> bool + Sync),
) -> Vec<(Raw, Option<Arc<[Raw]>>)> {
let scan_part = |part: &[Raw]| {
part.iter()
.take_while(|_| !abandoned())
.map(|handle| (*handle, scan(snapshot, handle).map(Arc::from)))
.collect::<Vec<_>>()
};
if threads <= 1 || handles.len() < 2 {
return scan_part(handles);
}
let chunk = handles.len().div_ceil(threads);
std::thread::scope(|scope| {
let workers: Vec<_> = handles
.chunks(chunk)
.map(|part| {
SPAWNED_THREADS.fetch_add(1, Ordering::SeqCst);
scope.spawn(move || scan_part(part))
})
.collect();
workers
.into_iter()
.flat_map(|worker| worker.join().expect("held-blob scan thread panicked"))
.collect()
})
}
pub struct HeldWalker {
stop: Option<Box<dyn FnOnce() + Send>>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl std::fmt::Debug for HeldWalker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HeldWalker").finish_non_exhaustive()
}
}
impl Drop for HeldWalker {
fn drop(&mut self) {
if let Some(stop) = self.stop.take() {
stop();
}
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
impl<T: HeldSource> HeldIndex<T> {
pub(crate) fn start_walker(self: &Arc<Self>, config: HeldWalkConfig) -> HeldWalker {
{
let mut state = self.lock();
state.walker.get_or_insert_with(Walker::default).threads += 1;
}
let stop = Arc::new(AtomicBool::new(false));
WALKER_THREADS.fetch_add(1, Ordering::SeqCst);
SPAWNED_THREADS.fetch_add(1, Ordering::SeqCst);
let index = Arc::clone(self);
let thread = std::thread::Builder::new()
.name("held-blob-walk".into())
.spawn({
let stop = Arc::clone(&stop);
move || {
index.run_walker(config, &stop);
WALKER_THREADS.fetch_sub(1, Ordering::SeqCst);
}
})
.expect("spawn the held-blob walker thread");
let index = Arc::clone(self);
HeldWalker {
stop: Some(Box::new(move || {
stop.store(true, Ordering::Relaxed);
let _state = index.lock();
index.wake.notify_all();
})),
thread: Some(thread),
}
}
fn run_walker(&self, config: HeldWalkConfig, stop: &AtomicBool) {
let mut due = Instant::now() + config.interval;
let mut owed = Handles::new();
loop {
let only = {
let mut state = self.lock();
loop {
if stop.load(Ordering::Relaxed) {
let back: Vec<Raw> = owed
.iter()
.filter(|collection| state.warming.get(collection).is_some())
.copied()
.collect();
let Some(walker) = &mut state.walker else {
return;
};
for collection in &back {
walker.requested.insert(&Entry::new(collection));
}
walker.threads -= 1;
if walker.threads == 0 {
state.walker = None;
state.release_warming();
} else if !back.is_empty() {
self.wake.notify_all();
}
return;
}
let Some(walker) = &mut state.walker else {
return;
};
let requested = !walker.requested.is_empty();
let now = Instant::now();
if requested || now >= due {
break;
}
state = self
.wake
.wait_timeout(state, due - now)
.expect("held-blob index is not poisoned")
.0;
}
let requested = std::mem::take(
&mut state
.walker
.as_mut()
.expect("the walker is attached")
.requested,
);
owed = requested.clone();
let only = if Instant::now() >= due {
due = Instant::now() + config.interval;
None
} else {
Some(requested)
};
only
};
if self.walk(config.threads, only.as_ref(), Some(stop)) {
owed = Handles::new();
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeSet, HashMap};
use std::convert::Infallible;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use anybytes::Bytes;
use ed25519_dalek::SigningKey;
use super::*;
use crate::blob::{Blob, BlobEncoding, IntoBlob, TryFromBlob};
use crate::capability::{CapabilityProof, CapabilityProofId, CapabilityResource};
use crate::collection::covered::Covered;
use crate::collection::{
CollectionCommit, CollectionDerive, CollectionMap, CollectionMerge, CollectionStore,
SourceLocator,
};
use crate::inline::InlineEncoding;
use crate::prelude::entity;
use crate::repo::memoryrepo::{MemoryStore, MemoryStoreSnapshot};
use crate::repo::{BlobInfo, BlobStorePut, CapabilityProofStore, SnapshotSource};
type Reads = Arc<Mutex<HashMap<Raw, usize>>>;
#[derive(Default)]
struct Gate {
closed: Mutex<bool>,
at: Mutex<Option<Raw>>,
opened: Condvar,
waiting: AtomicUsize,
}
struct Opener(Arc<Gate>);
impl Drop for Opener {
fn drop(&mut self) {
self.0.open();
}
}
impl Gate {
fn hold(self: &Arc<Self>) -> Opener {
*self.closed.lock().unwrap() = true;
Opener(Arc::clone(self))
}
fn hold_at(self: &Arc<Self>, handle: Raw) -> Opener {
*self.at.lock().unwrap() = Some(handle);
self.hold()
}
fn open(&self) {
*self.closed.lock().unwrap() = false;
self.opened.notify_all();
}
fn pass(&self, handle: &Raw) {
if std::thread::current().name() != Some("held-blob-walk") {
return;
}
if self.at.lock().unwrap().is_some_and(|at| at != *handle) {
return;
}
let mut closed = self.closed.lock().unwrap();
if *closed {
self.waiting.fetch_add(1, Ordering::SeqCst);
while *closed {
closed = self.opened.wait(closed).unwrap();
}
self.waiting.fetch_sub(1, Ordering::SeqCst);
}
}
fn await_walker(&self) {
let start = Instant::now();
while self.waiting.load(Ordering::SeqCst) == 0 {
assert!(
start.elapsed() < Duration::from_secs(30),
"the walker never reached the gate"
);
std::thread::sleep(Duration::from_millis(1));
}
}
}
#[derive(Default)]
struct Faults {
select: AtomicBool,
proofs: AtomicBool,
unreadable: Mutex<std::collections::HashSet<Raw>>,
}
#[derive(Debug)]
struct Fault;
impl std::fmt::Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "injected store fault")
}
}
impl std::error::Error for Fault {}
fn lift<V>(result: Result<V, Infallible>) -> Result<V, Fault> {
result.map_err(|never| match never {})
}
#[derive(Default)]
struct Counting {
inner: MemoryStore,
reads: Reads,
foreground: Reads,
gate: Arc<Gate>,
faults: Arc<Faults>,
}
#[derive(Clone)]
struct CountingSnapshot {
inner: MemoryStoreSnapshot,
reads: Reads,
foreground: Reads,
gate: Arc<Gate>,
faults: Arc<Faults>,
}
impl SnapshotSource for Counting {
type Snapshot = CountingSnapshot;
type SnapshotError = Infallible;
fn snapshot(&mut self) -> Result<Self::Snapshot, Self::SnapshotError> {
Ok(CountingSnapshot {
inner: self.inner.snapshot()?,
reads: self.reads.clone(),
foreground: self.foreground.clone(),
gate: self.gate.clone(),
faults: self.faults.clone(),
})
}
}
impl BlobStorePut for Counting {
type PutError = Infallible;
fn put<S, T>(&mut self, item: T) -> Result<Inline<Handle<S>>, Self::PutError>
where
S: BlobEncoding + 'static,
T: IntoBlob<S>,
Handle<S>: InlineEncoding,
{
self.inner.put(item)
}
}
impl CollectionStore for Counting {
type InsertError = <MemoryStore as CollectionStore>::InsertError;
fn insert(&mut self, record: CollectionRecord) -> Result<(), Self::InsertError> {
self.inner.insert(record)
}
}
impl CapabilityProofStore for Counting {
type InsertError = <MemoryStore as CapabilityProofStore>::InsertError;
fn insert_proof(&mut self, proof: CapabilityProof) -> Result<(), Self::InsertError> {
self.inner.insert_proof(proof)
}
}
impl StoreSnapshot for CountingSnapshot {
fn changes_since(&self, previous: &Self) -> StoreChanges {
self.inner.changes_since(&previous.inner)
}
}
impl BlobStoreGet for CountingSnapshot {
type GetError<E: std::error::Error + Send + Sync + 'static> =
<MemoryStoreSnapshot as BlobStoreGet>::GetError<E>;
fn get<V, S>(
&self,
handle: Inline<Handle<S>>,
) -> Result<V, Self::GetError<<V as TryFromBlob<S>>::Error>>
where
S: BlobEncoding + 'static,
V: TryFromBlob<S>,
Handle<S>: InlineEncoding,
{
self.gate.pass(&handle.raw);
if self.faults.unreadable.lock().unwrap().contains(&handle.raw) {
return Err(crate::blob::MemoryStoreGetError::NotFound(
crate::repo::MissingBlob {
handle: handle.transmute(),
},
));
}
*self.reads.lock().unwrap().entry(handle.raw).or_default() += 1;
if std::thread::current().name() != Some("held-blob-walk") {
*self
.foreground
.lock()
.unwrap()
.entry(handle.raw)
.or_default() += 1;
}
self.inner.get(handle)
}
}
impl BlobStoreList for CountingSnapshot {
type Iter<'a> = <MemoryStoreSnapshot as BlobStoreList>::Iter<'a>;
type Err = <MemoryStoreSnapshot as BlobStoreList>::Err;
fn blobs<'a>(&'a self) -> Self::Iter<'a> {
self.inner.blobs()
}
fn contains_blob<S>(&self, handle: Inline<Handle<S>>) -> Result<bool, Self::Err>
where
S: BlobEncoding + 'static,
Handle<S>: InlineEncoding,
{
self.inner.contains_blob(handle)
}
fn blob_info<S>(&self, handle: Inline<Handle<S>>) -> Result<Option<BlobInfo>, Self::Err>
where
S: BlobEncoding + 'static,
Handle<S>: InlineEncoding,
{
self.inner.blob_info(handle)
}
fn blobs_diff<'a>(&'a self, old: &Self) -> Self::Iter<'a> {
self.inner.blobs_diff(&old.inner)
}
}
impl RecordDelta for CountingSnapshot {
fn for_each_record_since(
&self,
since: Option<&Self>,
each: &mut dyn FnMut(&CollectionRecord),
) {
self.inner
.for_each_record_since(since.map(|since| &since.inner), each)
}
}
type Lift<V> = fn(Result<V, Infallible>) -> Result<V, Fault>;
impl CollectionRead for CountingSnapshot {
type RecordsError = Fault;
type RecordIter<'a> = std::iter::Map<
<MemoryStoreSnapshot as CollectionRead>::RecordIter<'a>,
Lift<CollectionRecord>,
>;
fn records<'a>(&'a self) -> Result<Self::RecordIter<'a>, Self::RecordsError> {
Ok(lift(self.inner.records())?.map(lift as Lift<CollectionRecord>))
}
fn collections(&self) -> Result<Vec<CollectionHandle>, Self::RecordsError> {
lift(self.inner.collections())
}
fn select_records(
&self,
selectors: &BTreeSet<CollectionRecordSelector>,
) -> Result<Vec<CollectionRecord>, Self::RecordsError> {
if self.faults.select.load(Ordering::SeqCst) {
return Err(Fault);
}
lift(self.inner.select_records(selectors))
}
}
impl CapabilityProofRead for CountingSnapshot {
type ProofsError = Fault;
type ProofIter<'a> = std::iter::Map<
<MemoryStoreSnapshot as CapabilityProofRead>::ProofIter<'a>,
Lift<CapabilityProof>,
>;
fn proofs<'a>(&'a self) -> Result<Self::ProofIter<'a>, Self::ProofsError> {
if self.faults.proofs.load(Ordering::SeqCst) {
return Err(Fault);
}
Ok(lift(self.inner.proofs())?.map(lift as Lift<CapabilityProof>))
}
fn proof(
&self,
id: CapabilityProofId,
) -> Result<Option<CapabilityProof>, Self::ProofsError> {
lift(self.inner.proof(id))
}
}
type Store = Covered<Counting>;
static WALKER_TESTS: Mutex<()> = Mutex::new(());
fn walker_guard() -> std::sync::MutexGuard<'static, ()> {
WALKER_TESTS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn key() -> SigningKey {
SigningKey::from_bytes(&[41; 32])
}
fn blob<S: SnapshotSource + BlobStorePut>(store: &mut S, bytes: &[u8]) -> Raw {
store
.put::<UnknownBlob, _>(Bytes::from_source(bytes.to_vec()))
.unwrap()
.raw
}
fn blob_naming<S: SnapshotSource + BlobStorePut>(
store: &mut S,
label: &[u8],
words: &[Raw],
) -> Raw {
store
.put::<UnknownBlob, _>(unresident_naming(label, words))
.unwrap()
.raw
}
fn unresident_naming(label: &[u8], words: &[Raw]) -> Bytes {
let mut bytes: Vec<u8> = words.iter().flatten().copied().collect();
bytes.extend_from_slice(&label[..label.len().min(31)]);
Bytes::from_source(bytes)
}
fn handle_of(bytes: &Bytes) -> Raw {
crate::blob::Blob::<UnknownBlob>::new(bytes.clone())
.get_handle()
.raw
}
fn collection<S: SnapshotSource + BlobStorePut>(store: &mut S, name: &str) -> CollectionHandle {
Inline::new(blob(store, format!("descriptor {name}").as_bytes()))
}
fn governed(store: &mut Store, name: &str, root: &SigningKey) -> CollectionHandle {
use crate::collection::{AdmissionPolicy, CollectionPolicy, CollectionStoreExt};
let direct = AdmissionPolicy::direct(root.verifying_key());
store
.collection(name, CollectionPolicy::new(direct.clone(), direct))
.unwrap()
.handle()
}
fn commit<S: CollectionStore>(
store: &mut S,
collection: CollectionHandle,
data: Raw,
metadata: Raw,
) {
store
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key(),
collection,
Inline::new(data),
Inline::new(metadata),
)))
.unwrap();
}
fn held_set(snapshot: &impl HeldRead, collection: CollectionHandle) -> BTreeSet<Raw> {
snapshot
.held(collection)
.expect("the collection is tracked")
.iter_ordered()
.copied()
.collect()
}
fn assert_closed(snapshot: &<Store as SnapshotSource>::Snapshot, c: CollectionHandle) {
let held = held_set(snapshot, c);
let blobs = &snapshot.inner().inner;
for parent in &held {
let Ok(body) = blobs.get::<Bytes, UnknownBlob>(Inline::new(*parent)) else {
continue;
};
for word in body.as_ref().chunks_exact(32) {
let child: Raw = word.try_into().expect("32-byte word");
if blobs
.contains_blob(Inline::<Handle<UnknownBlob>>::new(child))
.unwrap()
{
assert!(
held.contains(&child),
"held blob {:02x?} lacks its resident child {:02x?}",
&parent[..4],
&child[..4]
);
}
}
}
}
fn until(
store: &mut Store,
what: &str,
mut done: impl FnMut(&<Store as SnapshotSource>::Snapshot) -> bool,
) -> <Store as SnapshotSource>::Snapshot {
let start = Instant::now();
loop {
let snapshot = store.snapshot().unwrap();
if done(&snapshot) {
return snapshot;
}
assert!(
start.elapsed() < Duration::from_secs(30),
"timed out waiting for {what}"
);
std::thread::sleep(Duration::from_millis(2));
}
}
fn walker_config(interval: Duration) -> HeldWalkConfig {
HeldWalkConfig {
interval,
threads: 1,
}
}
fn reads(store: &Store, handle: &Raw) -> usize {
store
.reads
.lock()
.unwrap()
.get(handle)
.copied()
.unwrap_or(0)
}
#[test]
fn a_commit_holds_its_descriptor_payload_closure_and_metadata_archive() {
let mut store = Store::default();
let c = collection(&mut store, "closure");
let grandchild = blob(&mut store, b"grandchild");
let child = blob_naming(&mut store, b"child", &[grandchild]);
let absent = [7; 32];
let data = blob_naming(&mut store, b"data", &[child, absent]);
let described = blob(&mut store, b"named by metadata");
let metadata = blob_naming(&mut store, b"metadata", &[described]);
commit(&mut store, c, data, metadata);
store.track_held([c]);
let snapshot = store.snapshot().unwrap();
assert_eq!(
held_set(&snapshot, c),
BTreeSet::from([c.raw, data, child, grandchild, metadata, described])
);
assert_eq!(snapshot.held(Inline::new([9; 32])), None);
}
#[test]
fn merge_results_and_blobs_no_record_names_are_never_held() {
let mut store = Store::default();
let c = collection(&mut store, "merges");
let data = blob(&mut store, b"foundation");
let metadata = blob(&mut store, b"metadata");
commit(&mut store, c, data, metadata);
let under_merge = blob(&mut store, b"only a merge result reaches this");
let result = blob_naming(&mut store, b"merge result", &[under_merge, data]);
store
.insert(CollectionRecord::Merge(
CollectionMerge::sign(
&key(),
c,
[Inline::new(data), Inline::new([3; 32])],
Inline::new(result),
)
.unwrap(),
))
.unwrap();
let attachment = blob_naming(&mut store, b"attachment", &[data]);
let derived = collection(&mut store, "derived");
let output_child = blob(&mut store, b"under the output");
let output = blob_naming(&mut store, b"derive output", &[output_child]);
store
.insert(CollectionRecord::Derive(CollectionDerive::sign(
&key(),
derived,
SourceLocator::of(data),
Inline::new(output),
)))
.unwrap();
store.track_held([c, derived]);
let snapshot = store.snapshot().unwrap();
let held = held_set(&snapshot, c);
assert_eq!(held, BTreeSet::from([c.raw, data, metadata]));
for never in [result, under_merge, attachment] {
assert!(!held.contains(&never));
}
assert_eq!(
held_set(&snapshot, derived),
BTreeSet::from([derived.raw, output, output_child]),
"a DERIVE holds its output, never its source node"
);
}
#[test]
fn map_attachments_are_never_held_even_in_a_tracked_attached_collection() {
let mut store = Store::default();
let parent = collection(&mut store, "parent");
let data = blob(&mut store, b"foundation");
let metadata = blob(&mut store, b"metadata");
commit(&mut store, parent, data, metadata);
let attached = collection(&mut store, "attached");
let under = blob(&mut store, b"only an attachment reaches this");
let attachment = blob_naming(&mut store, b"attachment", &[under]);
for target in [attached, parent] {
store
.insert(CollectionRecord::Map(CollectionMap::sign(
&key(),
target,
Inline::new(data),
Inline::new(attachment),
)))
.unwrap();
}
store.track_held([parent, attached]);
let snapshot = store.snapshot().unwrap();
assert_eq!(
held_set(&snapshot, parent),
BTreeSet::from([parent.raw, data, metadata])
);
assert_eq!(
held_set(&snapshot, attached),
BTreeSet::from([attached.raw])
);
let later = blob_naming(&mut store, b"a later attachment", &[under]);
store
.insert(CollectionRecord::Map(CollectionMap::sign(
&key(),
attached,
Inline::new(data),
Inline::new(later),
)))
.unwrap();
let after = store.snapshot().unwrap();
assert_eq!(held_set(&after, attached), BTreeSet::from([attached.raw]));
assert_eq!(
held_set(&after, parent),
BTreeSet::from([parent.raw, data, metadata])
);
}
#[test]
fn a_blob_before_its_record_and_a_record_before_its_blob_are_both_held() {
let mut store = Store::default();
let c = collection(&mut store, "arrival order");
let metadata = blob(&mut store, b"metadata");
store.track_held([c]);
let child = blob(&mut store, b"early child");
let early = blob_naming(&mut store, b"early", &[child]);
let before = store.snapshot().unwrap();
assert!(!held_set(&before, c).contains(&early));
commit(&mut store, c, early, metadata);
let after = store.snapshot().unwrap();
assert!(held_set(&after, c).is_superset(&BTreeSet::from([early, child])));
let late_child = blob(&mut store, b"late payload child");
let late = unresident_naming(b"late payload", &[late_child]);
let late_handle = handle_of(&late);
commit(&mut store, c, late_handle, metadata);
let waiting = store.snapshot().unwrap();
assert!(!held_set(&waiting, c).contains(&late_handle));
store.put::<UnknownBlob, _>(late).unwrap();
let arrived = store.snapshot().unwrap();
assert!(held_set(&arrived, c).is_superset(&BTreeSet::from([late_handle, late_child])));
assert_eq!(
reads(&store, &late_handle),
2,
"one failed read, then one scan"
);
}
#[test]
fn a_late_child_resident_through_another_collection_joins_by_report() {
let mut store = Store::default();
let c1 = collection(&mut store, "first");
let c2 = collection(&mut store, "second");
let metadata = blob(&mut store, b"metadata");
let h_bytes = unresident_naming(b"late child", &[]);
let h = handle_of(&h_bytes);
let p = blob_naming(&mut store, b"parent", &[h]);
commit(&mut store, c1, p, metadata);
store.track_held([c1, c2]);
let first = store.snapshot().unwrap();
assert!(held_set(&first, c1).contains(&p));
store.put::<UnknownBlob, _>(h_bytes).unwrap();
commit(&mut store, c2, h, metadata);
let second = store.snapshot().unwrap();
assert!(held_set(&second, c2).contains(&h));
assert!(
!held_set(&second, c1).contains(&h),
"P was scanned before H arrived"
);
store.note_held(c1, Inline::new([5; 32]));
store.note_held(c1, Inline::new(h));
let reported = store.snapshot().unwrap();
assert!(held_set(&reported, c1).contains(&h));
assert!(!held_set(&reported, c1).contains(&[5; 32]));
assert_eq!(reads(&store, &h), 1, "H was read once, as C2's seed");
assert_eq!(reads(&store, &p), 1);
}
#[test]
fn a_nested_late_local_child_is_caught_by_the_walk_and_not_before() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "nested");
let metadata = blob(&mut store, b"metadata");
let h_bytes = unresident_naming(b"nested late child", &[]);
let h = handle_of(&h_bytes);
let p = blob_naming(&mut store, b"opaque intermediate", &[h]);
let r = blob_naming(&mut store, b"root payload", &[p]);
commit(&mut store, c, r, metadata);
store.track_held([c]);
let first = store.snapshot().unwrap();
assert!(held_set(&first, c).contains(&p));
store.put::<UnknownBlob, _>(h_bytes).unwrap();
let mut last = None;
for _ in 0..3 {
let snapshot = store.snapshot().unwrap();
assert!(!held_set(&snapshot, c).contains(&h), "no arrival rescans P");
last = Some(snapshot);
}
let before_walk = last.unwrap();
let generation = before_walk.held_generation();
store.walk_held(2).unwrap();
let walked = store.snapshot().unwrap();
assert!(held_set(&walked, c).contains(&h));
assert_ne!(walked.held_generation(), generation);
assert!(
!held_set(&before_walk, c).contains(&h),
"an existing snapshot never changes"
);
assert_eq!(before_walk.held_generation(), generation);
}
#[test]
fn a_scanned_shared_intermediate_joins_another_collection_without_a_read() {
let mut store = Store::default();
let c1 = collection(&mut store, "one");
let c2 = collection(&mut store, "two");
let metadata = blob(&mut store, b"metadata");
let q = blob(&mut store, b"shared leaf");
let p = blob_naming(&mut store, b"shared intermediate", &[q]);
let first_payload = blob_naming(&mut store, b"first payload", &[p]);
commit(&mut store, c1, first_payload, metadata);
store.track_held([c1, c2]);
let first = store.snapshot().unwrap();
assert!(held_set(&first, c1).is_superset(&BTreeSet::from([p, q])));
assert!(!held_set(&first, c2).contains(&p));
let second_payload = blob_naming(&mut store, b"second payload", &[p]);
commit(&mut store, c2, second_payload, metadata);
let second = store.snapshot().unwrap();
assert!(held_set(&second, c2).is_superset(&BTreeSet::from([second_payload, p, q])));
assert_eq!(reads(&store, &p), 1);
assert_eq!(reads(&store, &q), 1);
assert_eq!(reads(&store, &second_payload), 1);
}
#[test]
fn each_blob_is_read_at_most_once_between_walks() {
let _guard = walker_guard();
let mut store = Store::default();
let collections: Vec<_> = (0..3)
.map(|index| collection(&mut store, &format!("scan-once {index}")))
.collect();
let metadata = blob(&mut store, b"shared metadata");
let shared = blob(&mut store, b"shared everywhere");
store.track_held(collections.iter().copied());
let mut payloads = Vec::new();
for round in 0..5_u8 {
for (index, collection) in collections.iter().enumerate() {
let own = blob(&mut store, &[round, index as u8, 1]);
let payload = blob_naming(&mut store, &[round, index as u8, 2], &[own, shared]);
commit(&mut store, *collection, payload, metadata);
payloads.push((payload, own));
store.note_held(*collection, Inline::new(shared));
}
store.snapshot().unwrap();
store.snapshot().unwrap();
}
let counts = store.reads.lock().unwrap().clone();
assert!(counts.values().all(|count| *count == 1), "{counts:?}");
let snapshot = store.snapshot().unwrap();
let reachable: BTreeSet<Raw> = collections
.iter()
.flat_map(|collection| held_set(&snapshot, *collection))
.collect();
store.walk_held(3).unwrap();
let counts = store.reads.lock().unwrap().clone();
for handle in &reachable {
assert_eq!(counts[handle], 2, "a walk rereads each reachable blob once");
}
}
#[test]
fn capability_definitions_named_by_proofs_are_seeds_and_may_arrive_later() {
let mut store = Store::default();
let root = SigningKey::from_bytes(&[42; 32]);
let c = governed(&mut store, "proven", &root);
let delegate = SigningKey::from_bytes(&[43; 32]);
let definition_bytes = unresident_naming(b"capability definition", &[]);
let definition: Inline<Handle<SimpleArchive>> = Inline::new(handle_of(&definition_bytes));
store.track_held([c]);
store.snapshot().unwrap();
store
.insert_proof(CapabilityProof::new(
CapabilityResource::from(c),
&root,
definition,
delegate.verifying_key(),
))
.unwrap();
let proven = store.snapshot().unwrap();
assert!(!held_set(&proven, c).contains(&definition.raw));
store.put::<UnknownBlob, _>(definition_bytes).unwrap();
let arrived = store.snapshot().unwrap();
assert!(held_set(&arrived, c).contains(&definition.raw));
}
#[test]
fn a_lost_blob_resets_the_held_sets() {
let mut store = Store::default();
let c = collection(&mut store, "removal");
let metadata = blob(&mut store, b"metadata");
let child = blob(&mut store, b"disconnected child");
let parent = blob_naming(&mut store, b"parent", &[child]);
let data = blob_naming(&mut store, b"data", &[parent]);
commit(&mut store, c, data, metadata);
store.track_held([c]);
let before = store.snapshot().unwrap();
assert!(held_set(&before, c).contains(&child));
let kept: Vec<_> = before
.inner()
.blobs()
.map(|info| info.unwrap().handle)
.filter(|handle| handle.raw != parent)
.collect();
(*store).inner.blobs.keep(kept);
let after = store.snapshot().unwrap();
let held = held_set(&after, c);
assert!(!held.contains(&parent));
assert!(!held.contains(&child), "the child lost its only path");
assert!(held.contains(&data));
}
#[test]
fn a_report_is_lost_on_reopen_unless_a_record_reaches_the_blob() {
let file = tempfile::NamedTempFile::new().unwrap();
let mut pile = crate::repo::pile::Pile::open(file.path()).unwrap();
let c = collection(&mut pile, "reopen");
let metadata = blob(&mut pile, b"metadata");
let reported = blob(&mut pile, b"reported by a peer");
let data = blob(&mut pile, b"unrelated payload");
commit(&mut pile, c, data, metadata);
pile.track_held([c]);
pile.snapshot().unwrap();
pile.note_held(c, Inline::new(reported));
assert!(held_set(&pile.snapshot().unwrap(), c).contains(&reported));
pile.close().unwrap();
let mut pile = crate::repo::pile::Pile::open(file.path()).unwrap();
pile.track_held([c]);
assert!(!held_set(&pile.snapshot().unwrap(), c).contains(&reported));
let reaching = blob_naming(&mut pile, b"a payload naming it", &[reported]);
commit(&mut pile, c, reaching, metadata);
assert!(held_set(&pile.snapshot().unwrap(), c).contains(&reported));
pile.close().unwrap();
let mut pile = crate::repo::pile::Pile::open(file.path()).unwrap();
pile.track_held([c]);
assert!(
held_set(&pile.snapshot().unwrap(), c).contains(&reported),
"a record reaches it now"
);
pile.close().unwrap();
}
#[test]
fn short_lived_opens_start_no_thread() {
let _guard = walker_guard();
let spawned = held_threads_spawned();
assert_eq!(held_walker_threads(), 0);
let file = tempfile::NamedTempFile::new().unwrap();
let mut pile = crate::repo::pile::Pile::open(file.path()).unwrap();
let c = collection(&mut pile, "short-lived");
let data = blob(&mut pile, b"payload");
let metadata = blob(&mut pile, b"metadata");
commit(&mut pile, c, data, metadata);
pile.snapshot().unwrap();
pile.track_held([c]);
pile.snapshot().unwrap();
pile.walk_held(1).unwrap();
assert_eq!(held_threads_spawned(), spawned, "nothing was spawned");
pile.walk_held(2).unwrap();
assert_eq!(held_threads_spawned(), spawned + 2);
let walker = pile.start_held_walker(HeldWalkConfig::default());
assert_eq!(held_walker_threads(), 1);
assert_eq!(held_threads_spawned(), spawned + 3);
drop(walker);
assert_eq!(held_walker_threads(), 0);
pile.close().unwrap();
}
#[test]
fn the_start_up_walk_never_gates_and_new_records_publish_with_their_closures() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "background");
let metadata = blob(&mut store, b"metadata");
let mut expected = BTreeSet::from([c.raw, metadata]);
for index in 0..64_u32 {
let leaf = blob(&mut store, &index.to_be_bytes());
let data = blob_naming(&mut store, &index.to_le_bytes(), &[leaf]);
commit(&mut store, c, data, metadata);
expected.extend([leaf, data]);
}
let walker = store.start_held_walker(HeldWalkConfig {
interval: Duration::from_secs(3600),
threads: 2,
});
store.track_held([c]);
let first = store.snapshot().unwrap();
let first_answer = held_set(&first, c);
assert!(
first_answer.is_empty(),
"nothing was scanned before the walk"
);
let mut settled = None;
for _ in 0..2000 {
let snapshot = store.snapshot().unwrap();
if held_set(&snapshot, c) == expected {
settled = Some(snapshot);
break;
}
std::thread::sleep(Duration::from_millis(5));
}
let settled = settled.expect("the start-up walk publishes the whole closure");
assert_ne!(settled.held_generation(), first.held_generation());
assert_eq!(held_set(&first, c), first_answer);
let deep = blob(&mut store, b"deep under a new record");
let middle = blob_naming(&mut store, b"middle", &[deep]);
let payload = blob_naming(&mut store, b"new payload", &[middle]);
commit(&mut store, c, payload, metadata);
let next = store.snapshot().unwrap();
assert!(held_set(&next, c).is_superset(&BTreeSet::from([payload, middle, deep])));
drop(walker);
}
#[test]
fn peer_reports_wait_for_the_start_up_walk_instead_of_reading_in_the_snapshot() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "restart");
let metadata = blob(&mut store, b"metadata");
let mut expected = BTreeSet::from([c.raw, metadata]);
let mut reported = Vec::new();
for index in 0..32_u32 {
let leaf = blob(&mut store, &index.to_be_bytes());
let data = blob_naming(&mut store, &index.to_le_bytes(), &[leaf]);
commit(&mut store, c, data, metadata);
expected.extend([leaf, data]);
reported.extend([leaf, data]);
}
let fetched_leaf = blob(&mut store, b"fetched leaf");
let fetched = blob_naming(&mut store, b"fetched by an exact read", &[fetched_leaf]);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold();
store.track_held([c]);
let first = store.snapshot().unwrap();
store.gate.await_walker();
for handle in reported.iter().chain([&fetched, &[6; 32]]) {
store.note_held(c, Inline::new(*handle));
}
let during = store.snapshot().unwrap();
assert!(
store.reads.lock().unwrap().is_empty(),
"a snapshot read reported blobs while the start-up walk was owed"
);
assert!(held_set(&during, c).is_empty(), "the reports wait");
store.gate.open();
expected.extend([fetched, fetched_leaf]);
until(
&mut store,
"the walk and the deferred reports",
|snapshot| held_set(snapshot, c) == expected,
);
let counts = store.reads.lock().unwrap().clone();
assert_eq!(
counts.keys().copied().collect::<BTreeSet<_>>(),
expected,
"an absent report is never read"
);
assert!(counts.values().all(|count| *count == 1), "{counts:?}");
assert!(held_set(&first, c).is_empty());
drop(walker);
}
#[test]
fn a_seed_arriving_during_the_start_up_walk_is_reached_when_it_publishes() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "seed during walk");
let metadata = blob(&mut store, b"metadata");
let child = blob(&mut store, b"late payload child");
let late = unresident_naming(b"late payload", &[child]);
let late_handle = handle_of(&late);
commit(&mut store, c, late_handle, metadata);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold();
store.track_held([c]);
store.snapshot().unwrap();
store.gate.await_walker();
store.put::<UnknownBlob, _>(late).unwrap();
let arrived = store.snapshot().unwrap();
assert!(!held_set(&arrived, c).contains(&late_handle));
store.gate.open();
until(&mut store, "the late seed and its child", |snapshot| {
held_set(snapshot, c) == BTreeSet::from([c.raw, metadata, late_handle, child])
});
drop(walker);
}
#[test]
fn a_proof_routed_by_a_later_resource_descriptor_is_seeded_on_its_arrival() {
use crate::capability::policy::resource_policy;
use crate::collection::AdmissionPolicy;
let mut store = Store::default();
let root = SigningKey::from_bytes(&[44; 32]);
let c = governed(&mut store, "routed", &root);
let other = governed(&mut store, "routed elsewhere", &root);
let delegate = SigningKey::from_bytes(&[45; 32]).verifying_key();
let definition: Inline<Handle<SimpleArchive>> =
Inline::new(blob(&mut store, b"routed definition"));
let elsewhere: Inline<Handle<SimpleArchive>> =
Inline::new(blob(&mut store, b"definition routed elsewhere"));
let direct = AdmissionPolicy::direct(root.verifying_key());
let resource_facts = entity! {
resource_collection: c,
resource_policy*: direct.binding(definition),
}
.facts()
.clone();
let resource: Blob<SimpleArchive> = resource_facts.clone().to_blob();
let other_facts = entity! {
resource_collection: other,
resource_policy*: direct.binding(elsewhere),
}
.facts()
.clone();
let other_resource: Blob<SimpleArchive> = other_facts.clone().to_blob();
store.track_held([c]);
store.snapshot().unwrap();
for (resource, definition) in [
(resource.get_handle(), definition),
(other_resource.get_handle(), elsewhere),
] {
store
.insert_proof(CapabilityProof::new(
CapabilityResource::from(resource),
&root,
definition,
delegate,
))
.unwrap();
}
let unrouted = store.snapshot().unwrap();
assert!(!held_set(&unrouted, c).contains(&definition.raw));
store.put::<SimpleArchive, _>(resource_facts).unwrap();
store.put::<SimpleArchive, _>(other_facts).unwrap();
let routed = store.snapshot().unwrap();
let held = held_set(&routed, c);
assert!(held.contains(&definition.raw));
assert!(
!held.contains(&elsewhere.raw),
"that resource routes to another collection"
);
}
#[test]
fn a_failed_selection_leaves_a_collection_unsettled_rather_than_short() {
let mut store = Store::default();
let c = collection(&mut store, "selection fails");
let metadata = blob(&mut store, b"metadata");
let data = blob(&mut store, b"payload");
commit(&mut store, c, data, metadata);
store.faults.select.store(true, Ordering::SeqCst);
store.track_held([c]);
let failed = store.snapshot().unwrap();
assert_eq!(failed.held(c), None);
store.faults.select.store(false, Ordering::SeqCst);
let settled = store.snapshot().unwrap();
assert_eq!(
held_set(&settled, c),
BTreeSet::from([c.raw, data, metadata])
);
}
#[test]
fn a_failed_proof_read_is_retried_at_the_next_snapshot() {
let mut store = Store::default();
let root = SigningKey::from_bytes(&[46; 32]);
let c = governed(&mut store, "proofs fail", &root);
let definition: Inline<Handle<SimpleArchive>> =
Inline::new(blob(&mut store, b"definition"));
store.track_held([c]);
store.snapshot().unwrap();
store.faults.proofs.store(true, Ordering::SeqCst);
store
.insert_proof(CapabilityProof::new(
CapabilityResource::from(c),
&root,
definition,
SigningKey::from_bytes(&[47; 32]).verifying_key(),
))
.unwrap();
let failed = store.snapshot().unwrap();
assert!(!held_set(&failed, c).contains(&definition.raw));
store.faults.proofs.store(false, Ordering::SeqCst);
let retried = store.snapshot().unwrap();
assert_eq!(retried.changes_since(&failed), StoreChanges::NONE);
assert!(held_set(&retried, c).contains(&definition.raw));
}
#[test]
fn unrelated_arrivals_read_nothing() {
let mut store = Store::default();
let c = collection(&mut store, "unrelated");
let metadata = blob(&mut store, b"metadata");
let child = blob(&mut store, b"child");
let data = blob_naming(&mut store, b"data", &[child]);
commit(&mut store, c, data, metadata);
store.track_held([c]);
store.snapshot().unwrap();
let before = store.reads.lock().unwrap().clone();
for index in 0..16_u8 {
blob_naming(&mut store, &[b'u', index], &[data, child]);
blob(&mut store, &[b'v', index]);
store.snapshot().unwrap();
}
assert_eq!(*store.reads.lock().unwrap(), before);
}
#[test]
fn the_periodic_walk_catches_a_nested_late_child_nobody_reports() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "periodic");
let metadata = blob(&mut store, b"metadata");
let h_bytes = unresident_naming(b"periodic late child", &[]);
let h = handle_of(&h_bytes);
let p = blob_naming(&mut store, b"periodic intermediate", &[h]);
let r = blob_naming(&mut store, b"periodic root", &[p]);
commit(&mut store, c, r, metadata);
let interval = Duration::from_millis(200);
let walker = store.start_held_walker(walker_config(interval));
store.track_held([c]);
until(&mut store, "the start-up walk", |snapshot| {
held_set(snapshot, c).contains(&p)
});
store.put::<UnknownBlob, _>(h_bytes).unwrap();
let arrived = Instant::now();
until(&mut store, "a periodic walk", |snapshot| {
held_set(snapshot, c).contains(&h)
});
assert!(arrived.elapsed() < interval + Duration::from_secs(20));
drop(walker);
}
#[test]
fn a_newly_tracked_collection_is_walked_alone() {
let _guard = walker_guard();
let mut store = Store::default();
let mut sets = Vec::new();
for name in ["walked first", "walked second"] {
let c = collection(&mut store, name);
let metadata = blob(&mut store, format!("{name} metadata").as_bytes());
let leaf = blob(&mut store, format!("{name} leaf").as_bytes());
let data = blob_naming(&mut store, name.as_bytes(), &[leaf]);
commit(&mut store, c, data, metadata);
sets.push((c, BTreeSet::from([c.raw, metadata, leaf, data])));
}
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
for (c, expected) in &sets {
store.track_held([*c]);
until(&mut store, "the start-up walk", |snapshot| {
snapshot.held(*c).is_some() && held_set(snapshot, *c) == *expected
});
}
let counts = store.reads.lock().unwrap().clone();
for handle in &sets[0].1 {
assert_eq!(
counts[handle], 1,
"the first collection was not walked again"
);
}
drop(walker);
}
#[test]
fn a_reset_stops_the_walk_it_made_stale() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "stale walk");
let metadata = blob(&mut store, b"metadata");
let mut expected = BTreeSet::from([c.raw, metadata]);
for index in 0..32_u32 {
let leaf = blob(&mut store, &index.to_be_bytes());
let data = blob_naming(&mut store, &index.to_le_bytes(), &[leaf]);
commit(&mut store, c, data, metadata);
expected.extend([leaf, data]);
}
let forgotten = blob(&mut store, b"forgotten");
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold();
store.track_held([c]);
let before = store.snapshot().unwrap();
store.gate.await_walker();
let kept: Vec<_> = before
.inner()
.blobs()
.map(|info| info.unwrap().handle)
.filter(|handle| handle.raw != forgotten)
.collect();
(*store).inner.blobs.keep(kept);
store.snapshot().unwrap();
store.gate.open();
until(&mut store, "the walk after the reset", |snapshot| {
held_set(snapshot, c) == expected
});
let total: usize = store.reads.lock().unwrap().values().sum();
assert!(
total <= expected.len() + 1,
"{total} reads for {} blobs",
expected.len()
);
drop(walker);
}
#[test]
fn a_new_foundation_over_a_blob_the_walk_has_not_finished_holds_its_whole_closure() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "closure during walk");
let metadata = blob(&mut store, b"metadata");
let b = blob(&mut store, b"child the walk reads last");
let a = blob_naming(&mut store, b"scanned before its child", &[b]);
let root = blob_naming(&mut store, b"walked root", &[a]);
commit(&mut store, c, root, metadata);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold_at(b);
store.track_held([c]);
store.snapshot().unwrap();
store.gate.await_walker();
let during = store.snapshot().unwrap();
let p = blob_naming(&mut store, b"new foundation over A", &[a]);
commit(&mut store, c, p, metadata);
let after = store.snapshot().unwrap();
let held = held_set(&after, c);
assert!(held.contains(&p), "a new record publishes with its closure");
assert!(
held.contains(&b),
"P's closure was published without B, the resident child of A"
);
assert_closed(&after, c);
assert_closed(&during, c);
store.gate.open();
until(&mut store, "the start-up walk", |snapshot| {
held_set(snapshot, c) == BTreeSet::from([c.raw, metadata, root, a, b, p])
});
drop(walker);
}
#[test]
fn deferred_reports_are_read_by_the_walker_and_never_in_a_snapshot() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "deferred walk");
let metadata = blob(&mut store, b"metadata");
let data = blob(&mut store, b"payload");
commit(&mut store, c, data, metadata);
let leaf = blob(&mut store, b"fetched leaf");
let middle = blob_naming(&mut store, b"fetched middle", &[leaf]);
let fetched = blob_naming(&mut store, b"fetched by an exact read", &[middle]);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold();
store.track_held([c]);
store.snapshot().unwrap();
store.gate.await_walker();
store.note_held(c, Inline::new(fetched));
store.snapshot().unwrap();
store.gate.open();
let expected = BTreeSet::from([c.raw, metadata, data, fetched, middle, leaf]);
until(&mut store, "the walk and the deferred report", |snapshot| {
held_set(snapshot, c) == expected
});
for _ in 0..3 {
store.snapshot().unwrap();
}
let foreground = store.foreground.lock().unwrap().clone();
assert!(
foreground.is_empty(),
"a snapshot read {} blobs the walker owed",
foreground.len()
);
let counts = store.reads.lock().unwrap().clone();
assert!(counts.values().all(|count| *count == 1), "{counts:?}");
drop(walker);
}
#[test]
fn only_proofs_the_collections_authorization_keeps_are_seeds() {
for track_resource in [false, true] {
proofs_the_authorization_keeps(track_resource);
}
}
fn proofs_the_authorization_keeps(track_resource: bool) {
use crate::capability::policy::resource_policy;
use crate::collection::{AdmissionPolicy, CollectionPolicy, CollectionStoreExt};
let collection_root = SigningKey::from_bytes(&[50; 32]);
let resource_root = SigningKey::from_bytes(&[51; 32]);
let stranger = SigningKey::from_bytes(&[52; 32]);
let subject = SigningKey::from_bytes(&[53; 32]).verifying_key();
let mut store = Store::default();
let direct = AdmissionPolicy::direct(collection_root.verifying_key());
let c = store
.collection("audited", CollectionPolicy::new(direct.clone(), direct))
.unwrap()
.handle();
let mut definition = |name: &str| -> Inline<Handle<SimpleArchive>> {
Inline::new(blob(&mut store, name.as_bytes()))
};
let exact = definition("definition over C");
let wrong_root = definition("definition from a stranger");
let routed = definition("definition over a routed resource");
let split = definition("definition over a split route");
let foreign_root = definition("definition from C's root over R");
let resource_policy_of =
|definition| AdmissionPolicy::direct(resource_root.verifying_key()).binding(definition);
let resource = store
.put::<SimpleArchive, _>(
entity! {
resource_collection: c,
resource_policy*: resource_policy_of(routed),
}
.facts()
.clone(),
)
.unwrap();
let split_resource = store
.put::<SimpleArchive, _>(
(entity! { resource_collection: c }
+ entity! { resource_policy*: resource_policy_of(split) })
.facts()
.clone(),
)
.unwrap();
for (resource, root, definition) in [
(c, &collection_root, exact),
(c, &stranger, wrong_root),
(resource, &resource_root, routed),
(split_resource, &resource_root, split),
(resource, &collection_root, foreign_root),
] {
store
.insert_proof(CapabilityProof::new(
CapabilityResource::from(resource),
root,
definition,
subject,
))
.unwrap();
}
if track_resource {
store.track_held([c, resource]);
} else {
store.track_held([c]);
}
let held = held_set(&store.snapshot().unwrap(), c);
assert!(held.contains(&exact.raw));
assert!(
held.contains(&routed.raw),
"a routed proof seeds C (resource tracked: {track_resource})"
);
for irrelevant in [wrong_root, split, foreign_root] {
assert!(
!held.contains(&irrelevant.raw),
"a proof C's authorization rejects seeded C"
);
}
}
#[test]
fn views_differing_only_in_one_held_set_are_unequal() {
let c = [1u8; 32];
let view = |blob: Raw| {
let mut held = HeldBlobs::new();
held.insert(&Entry::new(&blob));
let mut sets = HeldSets::new();
sets.replace(&Entry::with_value(&c, held));
HeldView {
generation: 1,
sets,
}
};
assert_eq!(view([2; 32]), view([2; 32]));
assert_ne!(view([2; 32]), view([3; 32]));
}
#[test]
fn a_new_commit_repeating_owed_seeds_publishes_their_closure() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "repeated seeds");
let metadata = blob(&mut store, b"metadata");
let leaf = blob(&mut store, b"under A");
let a = blob_naming(&mut store, b"A", &[leaf]);
commit(&mut store, c, a, metadata);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold();
store.track_held([c]);
store.snapshot().unwrap();
store.gate.await_walker();
let fresh_metadata = blob(&mut store, b"new metadata M2");
commit(&mut store, c, a, fresh_metadata);
let after = store.snapshot().unwrap();
let held = held_set(&after, c);
assert!(held.contains(&fresh_metadata));
assert!(
held.contains(&a) && held.contains(&leaf),
"a new COMMIT repeating A was published without A's closure"
);
assert_closed(&after, c);
store.gate.open();
until(&mut store, "the start-up walk", |snapshot| {
held_set(snapshot, c) == BTreeSet::from([c.raw, metadata, fresh_metadata, a, leaf])
});
drop(walker);
}
#[test]
fn a_report_noted_as_the_start_up_walk_ends_is_read_by_the_walker() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "report at the end");
let metadata = blob(&mut store, b"metadata");
let data = blob(&mut store, b"payload");
commit(&mut store, c, data, metadata);
let leaf = blob(&mut store, b"late report leaf");
let middle = blob_naming(&mut store, b"late report middle", &[leaf]);
let fetched = blob_naming(&mut store, b"late report root", &[middle]);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold();
store.track_held([c]);
store.snapshot().unwrap();
store.gate.await_walker();
store.note_held(c, Inline::new(fetched));
store.gate.open();
let start = Instant::now();
while [c.raw, metadata, data]
.iter()
.any(|handle| reads(&store, handle) == 0)
{
assert!(
start.elapsed() < Duration::from_secs(30),
"the walk never ran"
);
std::thread::sleep(Duration::from_millis(2));
}
std::thread::sleep(Duration::from_millis(300));
let expected = BTreeSet::from([c.raw, metadata, data, fetched, middle, leaf]);
until(&mut store, "the report", |snapshot| {
held_set(snapshot, c) == expected
});
let foreground = store.foreground.lock().unwrap().clone();
assert!(
foreground.is_empty(),
"a snapshot read {} blobs of a report the walk owed",
foreground.len()
);
drop(walker);
}
#[test]
fn a_report_round_rereads_a_blob_the_older_snapshot_lacked() {
let _guard = walker_guard();
let mut store = Store::default();
let c1 = collection(&mut store, "negative first");
let c2 = collection(&mut store, "reported second");
let metadata = blob(&mut store, b"metadata");
let r_bytes = unresident_naming(b"absent at the walk's start", &[]);
let r = handle_of(&r_bytes);
commit(&mut store, c1, r, metadata);
let leaf = blob(&mut store, b"second leaf");
let data = blob_naming(&mut store, b"second payload", &[leaf]);
commit(&mut store, c2, data, metadata);
let walker = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let _opener = store.gate.hold_at(leaf);
store.track_held([c1, c2]);
store.snapshot().unwrap();
store.gate.await_walker();
store.put::<UnknownBlob, _>(r_bytes).unwrap();
store.snapshot().unwrap();
store.note_held(c2, Inline::new(r));
store.snapshot().unwrap();
store.gate.open();
until(&mut store, "C2 to hold the reported R", |snapshot| {
held_set(snapshot, c2).contains(&r)
});
drop(walker);
}
#[test]
fn a_held_blob_is_closed_under_the_edges_a_walk_learns() {
let _guard = walker_guard();
let mut store = Counting::default();
let c = collection(&mut store, "edge growth");
let metadata = blob(&mut store, b"metadata");
let b_bytes = unresident_naming(b"late child B", &[]);
let b = handle_of(&b_bytes);
let a = blob_naming(&mut store, b"A names B", &[b]);
let root = blob_naming(&mut store, b"root names A", &[a]);
commit(&mut store, c, root, metadata);
let index: Arc<HeldIndex<CountingSnapshot>> = Arc::new(HeldIndex::default());
index.track([c]);
index.observe(&store.snapshot().unwrap());
store.put::<UnknownBlob, _>(b_bytes).unwrap();
let now = store.snapshot().unwrap();
index.observe(&now);
let closed = |index: &HeldIndex<CountingSnapshot>| {
let state = index.lock();
let held = state.held.get(&c.raw).cloned().unwrap_or_default();
for parent in held.iter_ordered() {
for child in related(&state.edges, parent) {
assert!(
held.has_prefix(&child),
"a held blob has a cached child that is not held"
);
}
}
};
let _opener = store.gate.hold_at(b);
let walk = std::thread::Builder::new()
.name("held-blob-walk".into())
.spawn({
let index = Arc::clone(&index);
move || index.walk(1, None, None)
})
.unwrap();
store.gate.await_walker();
let p = blob_naming(&mut store, b"new foundation over A", &[a]);
commit(&mut store, c, p, metadata);
index.observe(&store.snapshot().unwrap());
closed(&index);
store.gate.open();
walk.join().unwrap();
let view = index.observe(&store.snapshot().unwrap());
closed(&index);
let held = view.held(c).unwrap();
assert!(held.has_prefix(&b) && held.has_prefix(&p));
}
#[test]
fn an_unreadable_resident_child_holds_back_its_parent_on_both_paths() {
let _guard = walker_guard();
for with_walker in [false, true] {
let mut store = Store::default();
let c = collection(&mut store, "unreadable child");
let metadata = blob(&mut store, b"metadata");
let z = blob(&mut store, b"indexed, every copy invalid");
let data = blob_naming(&mut store, b"names Z", &[z]);
commit(&mut store, c, data, metadata);
store.faults.unreadable.lock().unwrap().insert(z);
let walker = with_walker
.then(|| store.start_held_walker(walker_config(Duration::from_secs(3600))));
store.track_held([c]);
let settled = until(&mut store, "the collection to settle", |snapshot| {
snapshot.held(c).is_some()
&& held_set(snapshot, c).is_superset(&BTreeSet::from([c.raw, metadata]))
});
std::thread::sleep(Duration::from_millis(100));
let settled = if with_walker {
store.snapshot().unwrap()
} else {
settled
};
assert_closed(&settled, c);
assert!(!held_set(&settled, c).contains(&data));
drop(walker);
store.faults.unreadable.lock().unwrap().remove(&z);
store.walk_held(1).unwrap();
let walked = store.snapshot().unwrap();
assert!(held_set(&walked, c).is_superset(&BTreeSet::from([data, z])));
}
}
#[test]
fn proofs_judged_against_an_unreadable_descriptor_are_judged_again() {
let mut store = Store::default();
let root = SigningKey::from_bytes(&[48; 32]);
let c = governed(&mut store, "descriptor unreadable", &root);
let definition: Inline<Handle<SimpleArchive>> =
Inline::new(blob(&mut store, b"definition behind a bad read"));
store
.insert_proof(CapabilityProof::new(
CapabilityResource::from(c),
&root,
definition,
SigningKey::from_bytes(&[49; 32]).verifying_key(),
))
.unwrap();
store.faults.unreadable.lock().unwrap().insert(c.raw);
store.track_held([c]);
let first = store.snapshot().unwrap();
assert!(!held_set(&first, c).contains(&definition.raw));
store.faults.unreadable.lock().unwrap().remove(&c.raw);
store.walk_held(1).unwrap();
let walked = store.snapshot().unwrap();
assert!(
held_set(&walked, c).contains(&definition.raw),
"a proof judged against an unreadable descriptor was never judged again"
);
}
#[test]
fn a_walk_never_publishes_a_blob_lost_before_it_began() {
let _guard = walker_guard();
let mut store = Counting::default();
let c = collection(&mut store, "lost before the walk");
let metadata = blob(&mut store, b"metadata");
let lost = blob(&mut store, b"forgotten by the store");
let data = blob_naming(&mut store, b"names the lost blob", &[lost]);
commit(&mut store, c, data, metadata);
let index: Arc<HeldIndex<CountingSnapshot>> = Arc::new(HeldIndex::default());
index.track([c]);
let before = store.snapshot().unwrap();
index.observe(&before);
let (arrived, taken) = std::sync::mpsc::channel();
let (go, resume) = std::sync::mpsc::channel::<()>();
*index.walk_taken.lock().unwrap() = Some(Box::new(move || {
arrived.send(()).unwrap();
resume.recv().unwrap();
}));
let walker = index.start_walker(walker_config(Duration::from_millis(10)));
taken
.recv_timeout(Duration::from_secs(30))
.expect("the walker took a walk");
let kept: Vec<_> = before
.blobs()
.map(|info| info.unwrap().handle)
.filter(|handle| handle.raw != lost)
.collect();
store.inner.blobs.keep(kept);
index.observe(&store.snapshot().unwrap());
go.send(()).unwrap();
let start = Instant::now();
loop {
let view = index.observe(&store.snapshot().unwrap());
if let Some(held) = view.held(c).filter(|held| held.has_prefix(&data)) {
assert!(
!held.has_prefix(&lost),
"a walk published a blob the store lost before it began"
);
break;
}
assert!(
start.elapsed() < Duration::from_secs(30),
"no walk held the data"
);
std::thread::sleep(Duration::from_millis(2));
}
drop(walker);
}
#[test]
fn a_stopped_walker_leaves_no_stop_for_other_walks() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "stop owner");
let metadata = blob(&mut store, b"metadata");
let h_bytes = unresident_naming(b"late child", &[]);
let h = handle_of(&h_bytes);
let p = blob_naming(&mut store, b"parent of a late child", &[h]);
commit(&mut store, c, p, metadata);
store.track_held([c]);
store.snapshot().unwrap();
let first = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let second = store.start_held_walker(walker_config(Duration::from_secs(3600)));
drop(first);
let start = Instant::now();
while held_walker_threads() != 0 && start.elapsed() < Duration::from_secs(1) {
std::thread::sleep(Duration::from_millis(2));
}
drop(second);
assert_eq!(held_walker_threads(), 0);
store.put::<UnknownBlob, _>(h_bytes).unwrap();
store.walk_held(1).unwrap();
assert!(
held_set(&store.snapshot().unwrap(), c).contains(&h),
"an explicit walk after the walkers stopped did nothing"
);
}
#[test]
fn a_stopped_walker_hands_back_the_start_up_walk_it_took() {
let _guard = walker_guard();
let mut store = Store::default();
let c = collection(&mut store, "handed back");
let metadata = blob(&mut store, b"metadata");
let leaf = blob(&mut store, b"leaf");
let data = blob_naming(&mut store, b"data", &[leaf]);
commit(&mut store, c, data, metadata);
let mut first = store.start_held_walker(walker_config(Duration::from_secs(3600)));
let opener = store.gate.hold();
store.track_held([c]);
store.snapshot().unwrap();
store.gate.await_walker();
let second = store.start_held_walker(walker_config(Duration::from_secs(3600)));
(first.stop.take().expect("the first walker is running"))();
drop(opener);
drop(first);
until(
&mut store,
"the second walker to take the start-up walk",
|snapshot| held_set(snapshot, c) == BTreeSet::from([c.raw, metadata, data, leaf]),
);
drop(second);
}
#[test]
fn the_foundations_selector_matches_commits_and_derives_only() {
let mut store = crate::repo::memoryrepo::MemoryRepo::default();
let c: CollectionHandle = Inline::new([1; 32]);
let commit = CollectionRecord::Commit(CollectionCommit::sign(
&key(),
c,
Inline::new([2; 32]),
Inline::new([3; 32]),
));
let merge = CollectionRecord::Merge(
CollectionMerge::sign(
&key(),
c,
[Inline::new([2; 32]), Inline::new([4; 32])],
Inline::new([5; 32]),
)
.unwrap(),
);
let derive = CollectionRecord::Derive(CollectionDerive::sign(
&key(),
c,
SourceLocator::of([6; 32]),
Inline::new([7; 32]),
));
let map = CollectionRecord::Map(CollectionMap::sign(
&key(),
c,
Inline::new([2; 32]),
Inline::new([8; 32]),
));
for record in [commit, merge, derive, map] {
store.insert(record).unwrap();
}
let selected: BTreeSet<_> = store
.snapshot()
.unwrap()
.select_records(&BTreeSet::from([CollectionRecordSelector::Foundations(c)]))
.unwrap()
.into_iter()
.collect();
assert_eq!(selected, BTreeSet::from([commit, derive]));
}
}