use std::{convert::Infallible, fmt, sync::Arc};
use ic_memory::{
AllocationDeclaration, CommittedAllocations, RuntimeBootstrapError, RuntimeOpenError,
RuntimeStateError, StaticMemoryDeclaration, bootstrap_default_memory_manager,
committed_allocations, sealed_declaration_snapshot,
};
#[doc(hidden)]
pub fn ensure_default_memory_manager(authority: &str) -> Result<(), DatabaseBootstrapError> {
let allocations = match committed_allocations() {
Ok(allocations) => allocations,
Err(RuntimeOpenError::NotBootstrapped) => bootstrap_default_memory_manager()?,
Err(RuntimeOpenError::State(error)) => {
return Err(RuntimeBootstrapError::State(error).into());
}
Err(_) => {
return Err(
RuntimeBootstrapError::State(RuntimeStateError::InconsistentLifecycle).into(),
);
}
};
validate_committed_authority_declarations(authority, &allocations)?;
Ok(())
}
fn validate_committed_authority_declarations(
authority: &str,
allocations: &CommittedAllocations,
) -> Result<(), DatabaseBootstrapError> {
let snapshot =
sealed_declaration_snapshot().map_err(RuntimeBootstrapError::<Infallible>::Registry)?;
if authority_declarations_are_committed(
authority,
snapshot.registered_declarations(),
allocations.declarations(),
) {
Ok(())
} else {
Err(RuntimeBootstrapError::DeclarationSnapshotMismatch.into())
}
}
fn authority_declarations_are_committed(
authority: &str,
registrations: &[StaticMemoryDeclaration],
committed: &[AllocationDeclaration],
) -> bool {
let mut found = false;
for registration in registrations {
if registration.authority() != authority {
continue;
}
found = true;
if !committed.contains(registration.declaration()) {
return false;
}
}
found
}
#[derive(Clone, Debug)]
pub struct DatabaseBootstrapError {
source: Arc<RuntimeBootstrapError<Infallible>>,
}
impl DatabaseBootstrapError {
#[must_use]
pub fn cause(&self) -> &RuntimeBootstrapError<Infallible> {
&self.source
}
}
impl From<RuntimeBootstrapError<Infallible>> for DatabaseBootstrapError {
fn from(source: RuntimeBootstrapError<Infallible>) -> Self {
Self {
source: Arc::new(source),
}
}
}
impl fmt::Display for DatabaseBootstrapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.source.fmt(f)
}
}
impl std::error::Error for DatabaseBootstrapError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ic_memory::{
AllocationPolicy, AllocationSlotDescriptor, MemoryManagerConfig, MemoryManagerRangeMode,
PolicyIdentity, PolicyIdentityError, RuntimeBootstrapPolicy, StableKey,
bootstrap_default_memory_manager_with_config, default_memory_manager_memory_allocations,
register_static_memory_manager_declaration, register_static_memory_manager_range,
};
const TEST_AUTHORITY: &str = "icydb.bootstrap-adoption-test";
const TEST_STABLE_KEY: &str = "icydb.bootstrap_adoption_test.data.v1";
const TEST_MEMORY_ID: u8 = 100;
struct ExistingRuntimePolicy;
impl AllocationPolicy for ExistingRuntimePolicy {
type Error = Infallible;
fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
Ok(())
}
fn validate_slot(
&self,
_key: &StableKey,
_slot: &AllocationSlotDescriptor,
) -> Result<(), Self::Error> {
Ok(())
}
fn validate_reserved_slot(
&self,
_key: &StableKey,
_slot: &AllocationSlotDescriptor,
) -> Result<(), Self::Error> {
Ok(())
}
}
impl RuntimeBootstrapPolicy for ExistingRuntimePolicy {
fn runtime_bootstrap_identity(&self) -> Result<PolicyIdentity, PolicyIdentityError> {
PolicyIdentity::new("tests.existing-runtime-policy", 1)
}
}
fn declaration(key: &str, id: u8) -> AllocationDeclaration {
AllocationDeclaration::memory_manager(key, id, key)
.expect("test allocation declaration should admit")
}
fn registration(authority: &str, key: &str, id: u8) -> StaticMemoryDeclaration {
StaticMemoryDeclaration::new(authority, declaration(key, id))
.expect("test static registration should admit")
}
#[test]
fn authority_validation_requires_every_matching_declaration() {
let first = registration(TEST_AUTHORITY, "icydb.test.first.v1", 101);
let second = registration(TEST_AUTHORITY, "icydb.test.second.v1", 102);
let other = registration("other.framework", "other.framework.data.v1", 103);
let registrations = [first.clone(), second.clone(), other];
assert!(authority_declarations_are_committed(
TEST_AUTHORITY,
®istrations,
&[first.declaration().clone(), second.declaration().clone()],
));
assert!(!authority_declarations_are_committed(
TEST_AUTHORITY,
®istrations,
&[first.declaration().clone()],
));
assert!(!authority_declarations_are_committed(
"missing.authority",
®istrations,
&[first.declaration().clone(), second.declaration().clone()],
));
}
#[test]
fn adopts_runtime_bootstrapped_by_a_different_policy_and_bucket_size() {
register_static_memory_manager_range(
TEST_MEMORY_ID,
TEST_MEMORY_ID,
TEST_AUTHORITY,
MemoryManagerRangeMode::Reserved,
None,
)
.expect("test authority range should register");
register_static_memory_manager_declaration(
TEST_MEMORY_ID,
TEST_AUTHORITY,
"BootstrapAdoptionTest",
TEST_STABLE_KEY,
)
.expect("test allocation should register");
let upstream = bootstrap_default_memory_manager_with_config(
MemoryManagerConfig::new(16).expect("test bucket size should admit"),
&ExistingRuntimePolicy,
)
.expect("existing policy identity should bootstrap the shared runtime");
let generation = upstream.generation();
ensure_default_memory_manager(TEST_AUTHORITY)
.expect("IcyDB should adopt the upstream committed capability");
assert_eq!(
committed_allocations()
.expect("adopted allocations should remain available")
.generation(),
generation,
);
assert_eq!(
default_memory_manager_memory_allocations()
.expect("adopted runtime should report its persisted bucket size")
.bucket_size_pages,
16,
);
assert!(matches!(
bootstrap_default_memory_manager(),
Err(RuntimeBootstrapError::PolicyIdentityMismatch { .. })
));
}
}