use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt;
use ed25519_dalek::SigningKey;
use rand::seq::SliceRandom;
use crate::blob::encodings::simplearchive::SimpleArchive;
use crate::blob::encodings::UnknownBlob;
use crate::blob::Blob;
use crate::inline::encodings::ed25519::ED25519PublicKey;
use crate::inline::encodings::hash::Handle;
use crate::inline::Inline;
use crate::repo::async_store::AsyncBlobStoreAcquire;
use crate::repo::{BlobStoreGet, BlobStoreList, Store, StoreRead};
use crate::trible::Fragment;
use super::operation_snapshot::OperationFrontier;
use super::{
descriptor, Collection, CollectionData, CollectionEncoding, CollectionHandle, DeriveMapping,
MapMapping,
};
#[cfg(test)]
use super::{CanonicalDerivation, CollectionDerivation};
type BoxError = Box<dyn Error + Send + Sync + 'static>;
#[derive(Debug)]
pub enum CollectionRealizationError {
Storage {
operation: &'static str,
source: BoxError,
},
InvalidCover(String),
Resolution(String),
IncompleteCover {
missing: Vec<CollectionData>,
unsupported_members: Vec<CollectionData>,
},
UnauthorizedProducer {
collection: CollectionHandle,
},
Derive {
input: CollectionData,
reason: String,
},
Merge {
inputs: Vec<CollectionData>,
reason: String,
},
MissingDependency {
member: CollectionData,
},
Unmappable {
blocked: Vec<(CollectionData, String)>,
},
Stalled {
cover: Vec<CollectionData>,
},
HostMismatch {
host: Option<Inline<ED25519PublicKey>>,
signer: Inline<ED25519PublicKey>,
},
}
impl CollectionRealizationError {
pub(super) fn storage(
operation: &'static str,
source: impl Error + Send + Sync + 'static,
) -> Self {
Self::Storage {
operation,
source: Box::new(source),
}
}
}
const NAMED_IN_ERROR: usize = 4;
fn name_some(entries: impl IntoIterator<Item = String>) -> String {
let entries: Vec<_> = entries.into_iter().collect();
let total = entries.len();
let shown = entries
.iter()
.take(NAMED_IN_ERROR)
.cloned()
.collect::<Vec<_>>()
.join(", ");
match total.saturating_sub(NAMED_IN_ERROR) {
0 => shown,
rest => format!("{shown}, and {rest} more"),
}
}
impl fmt::Display for CollectionRealizationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Storage { operation, source } => write!(formatter, "{operation}: {source}"),
Self::InvalidCover(reason) => write!(formatter, "invalid exact support: {reason}"),
Self::Resolution(reason) => write!(formatter, "resolve collection: {reason}"),
Self::IncompleteCover {
missing,
unsupported_members,
} => write!(
formatter,
"collection realization is incomplete ({} missing target element(s): {}; {} unsupported foundational member(s): {})",
missing.len(),
name_some(missing.iter().map(|m| hex::encode_upper(m.raw))),
unsupported_members.len(),
name_some(unsupported_members.iter().map(|m| hex::encode_upper(m.raw))),
),
Self::UnauthorizedProducer { collection } => write!(
formatter,
"collection {} requires an admitted WRITE producer to realize missing support",
hex::encode_upper(collection.raw),
),
Self::Derive { input, reason } => write!(
formatter,
"derive source element {}: {reason}",
hex::encode_upper(input.raw),
),
Self::Merge { inputs, reason } => write!(
formatter,
"join {} collection element(s) {}: {reason}",
inputs.len(),
name_some(inputs.iter().map(|m| hex::encode_upper(m.raw))),
),
Self::MissingDependency { member } => write!(
formatter,
"mapping requires resident blob {}",
hex::encode_upper(member.raw),
),
Self::Unmappable { blocked } => write!(
formatter,
"{} own source foundation(s) have no leaf the mapping can represent: {}",
blocked.len(),
name_some(
blocked
.iter()
.map(|(member, reason)| format!("{} ({reason})", hex::encode_upper(member.raw))),
),
),
Self::Stalled { cover } => write!(
formatter,
"collection operation repeated an unchanged {}-member cover",
cover.len(),
),
Self::HostMismatch { host, signer } => write!(
formatter,
"maintenance signs with {} but the store folds the MERGEs of {}; open the store as the signing key",
hex::encode_upper(signer.raw),
host.map_or_else(|| "no key".to_owned(), |host| hex::encode_upper(host.raw)),
),
}
}
}
impl Error for CollectionRealizationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Storage { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
pub(super) fn producer_is_admitted<R, E>(
snapshot: &R,
target: Collection<E>,
signing_key: &SigningKey,
) -> Result<bool, CollectionRealizationError>
where
R: StoreRead,
E: CollectionEncoding,
{
target
.writer_is_admitted(snapshot, signing_key.verifying_key())
.map_err(|error| {
CollectionRealizationError::storage("check target producer WRITE authority", error)
})
}
pub(super) struct Lineage {
pub(super) foundation: Collection<SimpleArchive>,
pub(super) descriptors: BTreeMap<CollectionHandle, Fragment>,
pub(super) source_by_target: BTreeMap<CollectionHandle, CollectionHandle>,
}
impl Lineage {
pub(super) fn descriptor(&self, collection: CollectionHandle) -> &Fragment {
self.descriptors
.get(&collection)
.expect("loaded lineage contains every descriptor")
}
}
pub(super) fn load_lineage<R, E>(
snapshot: &R,
target: Collection<E>,
) -> Result<Lineage, CollectionRealizationError>
where
R: BlobStoreGet + BlobStoreList,
E: CollectionEncoding,
{
let lineage = load_record_lineage(snapshot, target.handle())?;
super::encoding::validate_descriptor_type::<E>(lineage.descriptor(target.handle())).map_err(
|error| {
CollectionRealizationError::Resolution(format!(
"target descriptor has the wrong representation: {error}"
))
},
)?;
Ok(lineage)
}
pub(super) fn load_record_lineage<R>(
snapshot: &R,
target: CollectionHandle,
) -> Result<Lineage, CollectionRealizationError>
where
R: BlobStoreGet + BlobStoreList,
{
let mut descriptors = BTreeMap::new();
let mut source_by_target = BTreeMap::new();
let mut collections = BTreeSet::new();
let mut cursor = target;
loop {
if !collections.insert(cursor) {
return Err(CollectionRealizationError::Resolution(format!(
"collection descriptor ancestry contains a cycle at {}",
hex::encode_upper(cursor.raw),
)));
}
if !snapshot.contains_blob(cursor).map_err(|error| {
CollectionRealizationError::storage("inspect collection descriptor residency", error)
})? {
return Err(CollectionRealizationError::MissingDependency {
member: Handle::<SimpleArchive>::to_hash(cursor),
});
}
let loaded = super::api::load_collection_descriptor(snapshot, cursor).map_err(|error| {
CollectionRealizationError::Resolution(format!(
"load collection descriptor {}: {error}",
hex::encode_upper(cursor.raw),
))
})?;
if !descriptor::parents(loaded.fragment.facts())
.map_err(|error| {
CollectionRealizationError::Resolution(format!(
"decode collection parent for {}: {error}",
hex::encode_upper(cursor.raw),
))
})?
.is_empty()
{
return Err(CollectionRealizationError::InvalidCover(format!(
"collection {} is an attached collection; maintain it through its mapping",
hex::encode_upper(cursor.raw),
)));
}
let source = descriptor::source(loaded.fragment.facts()).map_err(|error| {
CollectionRealizationError::Resolution(format!(
"decode collection source for {}: {error}",
hex::encode_upper(cursor.raw),
))
})?;
descriptors.insert(cursor, loaded.fragment);
match source {
Some(source) => {
source_by_target.insert(cursor, source);
cursor = source;
}
None => {
let root = descriptors
.get(&cursor)
.expect("root descriptor was inserted before inspection");
super::encoding::validate_descriptor_type::<SimpleArchive>(root).map_err(
|error| {
CollectionRealizationError::Resolution(format!(
"collection ancestry terminates in a non-SimpleArchive root: {error}"
))
},
)?;
return Ok(Lineage {
foundation: Collection::from_handle(cursor),
descriptors,
source_by_target,
});
}
}
}
}
#[cfg(test)]
pub(crate) fn foundation<R, E>(
snapshot: &R,
target: Collection<E>,
) -> Result<Collection<SimpleArchive>, CollectionRealizationError>
where
R: StoreRead,
E: CollectionEncoding,
{
Ok(load_lineage(snapshot, target)?.foundation)
}
#[cfg(test)]
fn ensure_resident_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store,
M: DeriveMapping,
{
let snapshot = store
.snapshot()
.map_err(|error| CollectionRealizationError::storage("freeze mapping frontier", error))?;
let mut frontier = OperationFrontier::new(snapshot);
ensure_resident_in_frontier_with::<S, M>(
store,
target,
signing_key,
&Unfetched::default(),
&mut Vec::new(),
&mut frontier,
)
}
#[cfg(test)]
fn ensure_resident<S, T>(
store: &mut S,
target: Collection<T>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store,
T: CollectionDerivation,
{
ensure_resident_with::<S, CanonicalDerivation<T>>(store, target, signing_key)
}
fn ensure_resident_in_frontier_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
signing_key: &SigningKey,
unfetched: &Unfetched,
wanted: &mut Vec<Vec<CollectionData>>,
frontier: &mut OperationFrontier<S::Snapshot>,
) -> Result<(), CollectionRealizationError>
where
S: Store,
M: DeriveMapping,
{
super::maintenance::maintain_derived::<S, M>(
store,
target,
signing_key,
unfetched,
wanted,
frontier,
false,
)
}
#[cfg(test)]
fn maintain_resident_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store,
M: DeriveMapping,
{
let snapshot = store.snapshot().map_err(|error| {
CollectionRealizationError::storage("freeze maintenance frontier", error)
})?;
let mut frontier = OperationFrontier::new(snapshot);
maintain_resident_in_frontier_with::<S, M>(
store,
target,
signing_key,
&Unfetched::default(),
&mut Vec::new(),
&mut frontier,
)
}
#[cfg(test)]
fn maintain_resident<S, T>(
store: &mut S,
target: Collection<T>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store,
T: CollectionDerivation,
{
maintain_resident_with::<S, CanonicalDerivation<T>>(store, target, signing_key)
}
fn maintain_resident_in_frontier_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
signing_key: &SigningKey,
unfetched: &Unfetched,
wanted: &mut Vec<Vec<CollectionData>>,
frontier: &mut OperationFrontier<S::Snapshot>,
) -> Result<(), CollectionRealizationError>
where
S: Store,
M: DeriveMapping,
{
super::maintenance::maintain_derived::<S, M>(
store,
target,
signing_key,
unfetched,
wanted,
frontier,
true,
)
}
pub(crate) async fn acquire_missing<S>(
store: &mut S,
attempted: &mut BTreeSet<CollectionData>,
member: CollectionData,
) -> Result<bool, CollectionRealizationError>
where
S: AsyncBlobStoreAcquire,
{
if !attempted.insert(member) {
return Ok(false);
}
store
.acquire(Handle::<UnknownBlob>::from_hash(member))
.await
.map_err(|error| {
CollectionRealizationError::storage("acquire exact blob dependency", error)
})
.map(|bytes| bytes.is_some())
}
pub(crate) async fn acquire_authority<S, E>(
store: &mut S,
target: Collection<E>,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
E: CollectionEncoding,
{
let mut attempted = BTreeSet::new();
loop {
let snapshot = store.snapshot().map_err(|error| {
CollectionRealizationError::storage("observe authority definitions", error)
})?;
let lineage = match load_lineage(&snapshot, target) {
Ok(lineage) => lineage,
Err(CollectionRealizationError::MissingDependency { member }) => {
drop(snapshot);
if acquire_missing(store, &mut attempted, member).await? {
continue;
}
return Err(CollectionRealizationError::MissingDependency { member });
}
Err(error) => return Err(error),
};
let mut definitions = BTreeSet::new();
let mut roots = BTreeSet::new();
let mut selected = BTreeSet::from([target.handle()]);
selected.extend(lineage.source_by_target.get(&target.handle()).copied());
for collection in selected {
let descriptor = lineage.descriptor(collection);
for (definition, policy) in descriptor::capability_policies(descriptor.facts(), None) {
definitions.insert(Handle::<SimpleArchive>::to_hash(definition));
if let Some(keys) = policy.roots() {
roots.extend(keys.iter().map(|key| (collection.raw, key.to_bytes())));
}
}
}
for proof in crate::repo::CapabilityProofRead::proofs(&snapshot).map_err(|error| {
CollectionRealizationError::storage("select authority proof definitions", error)
})? {
let proof = proof.map_err(|error| {
CollectionRealizationError::storage("read authority proof definitions", error)
})?;
if roots.contains(&(proof.resource().into_bytes(), proof.root_key().to_bytes())) {
definitions.extend(proof.blob_references().map(Handle::<UnknownBlob>::to_hash));
}
}
let missing = definitions
.into_iter()
.filter_map(|member| {
match snapshot.contains_blob(Handle::<UnknownBlob>::from_hash(member)) {
Ok(true) => None,
Ok(false) => Some(Ok(member)),
Err(error) => Some(Err(CollectionRealizationError::storage(
"inspect authority definition residency",
error,
))),
}
})
.collect::<Result<Vec<_>, _>>()?;
drop(snapshot);
for member in missing {
let _ = acquire_missing(store, &mut attempted, member).await?;
}
return Ok(());
}
}
pub(crate) async fn ensure_root<S>(
store: &mut S,
target: Collection<SimpleArchive>,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
{
let mut attempted = BTreeSet::new();
loop {
let snapshot = store.snapshot().map_err(|error| {
CollectionRealizationError::storage("observe root realization", error)
})?;
let result = super::maintenance::root_acquisitions(&snapshot, target);
drop(snapshot);
let missing = match result {
Ok(None) => return Ok(()),
Ok(Some(missing)) => missing,
Err(CollectionRealizationError::MissingDependency { member }) => vec![member],
Err(error) => return Err(error),
};
let previous_attempts = attempted.len();
for member in missing {
let _ = acquire_missing(store, &mut attempted, member).await?;
}
if attempted.len() == previous_attempts {
let snapshot = store.snapshot().map_err(|error| {
CollectionRealizationError::storage("observe incomplete root realization", error)
})?;
return super::maintenance::root_incomplete(&snapshot, target);
}
}
}
const OPTIONAL_FETCH_FAILURES: usize = 8;
#[derive(Default)]
pub(super) struct Unfetched {
pub(super) unavailable: BTreeSet<CollectionData>,
pub(super) failed: BTreeSet<CollectionData>,
}
impl Unfetched {
pub(super) fn contains(&self, member: &CollectionData) -> bool {
self.unavailable.contains(member) || self.failed.contains(member)
}
}
async fn acquiring<S, F>(store: &mut S, mut operation: F) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
F: FnMut(
&mut S,
&Unfetched,
&mut Vec<Vec<CollectionData>>,
&mut OperationFrontier<S::Snapshot>,
) -> Result<(), CollectionRealizationError>,
{
let mut attempted = BTreeSet::new();
let mut unfetched = Unfetched::default();
let mut failures = 0;
let mut broken = None;
loop {
let mut wanted = Vec::new();
let result = {
let mut frontier = OperationFrontier::new(store.snapshot().map_err(|error| {
CollectionRealizationError::storage("freeze operation control snapshot", error)
})?);
operation(store, &unfetched, &mut wanted, &mut frontier)
};
let reported = match result {
Ok(()) => None,
Err(error @ CollectionRealizationError::Storage { .. }) => return Err(error),
Err(CollectionRealizationError::MissingDependency { member })
if !attempted.contains(&member) =>
{
ask(store, &mut attempted, &mut unfetched, &mut broken, member).await;
continue;
}
Err(error) => Some(error),
};
if store.acquires_remotely() && failures < OPTIONAL_FETCH_FAILURES {
wanted.shuffle(&mut rand::thread_rng());
let asked = ask_wanted(
store,
wanted,
&mut attempted,
&mut unfetched,
&mut broken,
&mut failures,
)
.await;
if asked {
continue;
}
}
return match (broken, reported) {
(_, Some(error @ CollectionRealizationError::HostMismatch { .. })) => Err(error),
(Some(error), _) | (None, Some(error)) => Err(error),
(None, None) => Ok(()),
};
}
}
async fn ask_wanted<S>(
store: &mut S,
wanted: Vec<Vec<CollectionData>>,
attempted: &mut BTreeSet<CollectionData>,
unfetched: &mut Unfetched,
broken: &mut Option<CollectionRealizationError>,
failures: &mut usize,
) -> bool
where
S: AsyncBlobStoreAcquire,
{
let mut asked = false;
for group in wanted {
if *failures >= OPTIONAL_FETCH_FAILURES {
break;
}
if group
.iter()
.any(|member| attempted.contains(member) && !unfetched.contains(member))
{
continue;
}
for member in group {
if attempted.contains(&member) {
continue;
}
asked = true;
if ask(store, attempted, unfetched, broken, member).await {
break;
}
*failures += 1;
}
}
asked
}
async fn ask<S>(
store: &mut S,
attempted: &mut BTreeSet<CollectionData>,
unfetched: &mut Unfetched,
broken: &mut Option<CollectionRealizationError>,
member: CollectionData,
) -> bool
where
S: AsyncBlobStoreAcquire,
{
match acquire_missing(store, attempted, member).await {
Ok(true) => true,
Ok(false) => {
unfetched.unavailable.insert(member);
false
}
Err(error) => {
unfetched.failed.insert(member);
broken.get_or_insert(error);
false
}
}
}
pub(crate) async fn ensure_acquiring_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
M: DeriveMapping,
{
acquiring(store, |store, unfetched, wanted, frontier| {
ensure_resident_in_frontier_with::<S, M>(
store,
target,
signing_key,
unfetched,
wanted,
frontier,
)
})
.await
}
pub(crate) async fn maintain_acquiring_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
M: DeriveMapping,
{
acquiring(store, |store, unfetched, wanted, frontier| {
maintain_resident_in_frontier_with::<S, M>(
store,
target,
signing_key,
unfetched,
wanted,
frontier,
)
})
.await
}
pub(crate) async fn rederive_acquiring_with<S, M>(
store: &mut S,
target: Collection<M::Target>,
source: CollectionHandle,
foundations: &BTreeSet<CollectionData>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
M: DeriveMapping,
{
let mut done = BTreeSet::new();
acquiring(store, |store, unfetched, _wanted, frontier| {
super::maintenance::rederive::<S, M>(
store,
target,
source,
foundations,
&mut done,
signing_key,
unfetched,
frontier,
)
})
.await
}
pub(crate) async fn maintain_root_acquiring<S, E>(
store: &mut S,
target: Collection<E>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
E: CollectionEncoding,
{
acquiring(store, |store, _unfetched, _wanted, frontier| {
super::maintenance::carry_root(store, target, signing_key, frontier)
})
.await
}
pub(crate) async fn acquire_attached_lineage<S, E>(
store: &mut S,
attached: Collection<E>,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
E: CollectionEncoding,
{
let mut attempted = BTreeSet::new();
loop {
let snapshot = store.snapshot().map_err(|error| {
CollectionRealizationError::storage("observe attached lineage", error)
})?;
let result = super::maintenance::attached_lineage(&snapshot, attached);
drop(snapshot);
match result {
Ok(_) => return Ok(()),
Err(CollectionRealizationError::MissingDependency { member }) => {
if acquire_missing(store, &mut attempted, member).await? {
continue;
}
return Err(CollectionRealizationError::MissingDependency { member });
}
Err(error) => return Err(error),
}
}
}
pub(crate) async fn attach_acquiring_with<S, M>(
store: &mut S,
attached: Collection<M::Target>,
signing_key: &SigningKey,
) -> Result<(), CollectionRealizationError>
where
S: Store + AsyncBlobStoreAcquire,
M: MapMapping,
{
acquiring(store, |store, unfetched, _wanted, frontier| {
super::maintenance::attach_frontier::<S, M>(
store,
attached,
signing_key,
unfetched,
frontier,
)
})
.await
}
pub(super) fn data_identity<E: CollectionEncoding>(blob: &Blob<E>) -> CollectionData {
Handle::<E>::to_hash(blob.get_handle())
}
#[cfg(test)]
mod tests;