use crate::db::{
MemoryBootstrapAdmissionError, MemoryRuntimeBindingError, bind_memory_runtime, memory_runtime,
prepare_memory_bootstrap,
};
use std::{fmt, rc::Rc, sync::Arc};
use ic_memory::{
BootstrapAdmission, MemoryAllocationPool, MemoryAllocationPoolError, MemoryDeclarationError,
MemoryManagerConfig, MemoryManagerSlot, MemoryResolutionError, MemoryRuntime, PolicyIdentity,
RuntimeAdoptionError, RuntimeBootstrapError, RuntimeBootstrapPolicy, RuntimeConstructionError,
SealedDeclarationSnapshot, StableKey,
};
use ic_memory::DefaultMemoryImpl;
#[doc(hidden)]
pub fn ensure_default_memory_manager(
authority: &str,
requirements: &SealedDeclarationSnapshot,
bucket_size_pages: u16,
pool: impl FnOnce() -> Result<MemoryAllocationPool, MemoryAllocationPoolError>,
) -> Result<(), DatabaseBootstrapError> {
let runtime = if let Some(runtime) = memory_runtime() {
runtime
} else {
let config = MemoryManagerConfig::new(bucket_size_pages)?;
let pool = pool()?;
if let Some(runtime) = memory_runtime() {
runtime
} else {
let mut runtime = MemoryRuntime::new_with_config(DefaultMemoryImpl::default(), config)?;
runtime.bootstrap(requirements, &pool, &DatabaseMemoryPolicy)?;
let runtime = Rc::new(runtime);
bind_memory_runtime(Rc::clone(&runtime))?;
runtime
}
};
runtime.verify_authority(requirements, authority)?;
Ok(())
}
struct DatabaseMemoryPolicy;
impl RuntimeBootstrapPolicy for DatabaseMemoryPolicy {
type Error = MemoryBootstrapAdmissionError;
fn validate_allocation(&self, _: &StableKey, _: &MemoryManagerSlot) -> Result<(), Self::Error> {
Ok(())
}
fn prepare_bootstrap(&self, admission: &mut BootstrapAdmission<'_>) -> Result<(), Self::Error> {
prepare_memory_bootstrap(admission)
}
fn runtime_bootstrap_identity(&self) -> PolicyIdentity {
PolicyIdentity::new([
162, 211, 99, 60, 182, 106, 249, 175, 69, 210, 132, 12, 241, 103, 92, 177, 182, 86, 63,
5, 157, 250, 84, 228, 24, 141, 250, 92, 126, 87, 89, 68,
])
}
}
#[derive(Clone, Debug)]
pub enum DatabaseBootstrapError {
Construction(Arc<RuntimeConstructionError>),
Binding(MemoryRuntimeBindingError),
Bootstrap(Arc<RuntimeBootstrapError<MemoryBootstrapAdmissionError>>),
Adoption(Arc<RuntimeAdoptionError>),
}
impl From<MemoryRuntimeBindingError> for DatabaseBootstrapError {
fn from(source: MemoryRuntimeBindingError) -> Self {
Self::Binding(source)
}
}
impl From<RuntimeConstructionError> for DatabaseBootstrapError {
fn from(source: RuntimeConstructionError) -> Self {
Self::Construction(Arc::new(source))
}
}
impl From<RuntimeAdoptionError> for DatabaseBootstrapError {
fn from(source: RuntimeAdoptionError) -> Self {
Self::Adoption(Arc::new(source))
}
}
impl From<MemoryDeclarationError> for DatabaseBootstrapError {
fn from(source: MemoryDeclarationError) -> Self {
RuntimeBootstrapError::Resolution(MemoryResolutionError::Declarations(source)).into()
}
}
impl From<MemoryAllocationPoolError> for DatabaseBootstrapError {
fn from(source: MemoryAllocationPoolError) -> Self {
RuntimeBootstrapError::Resolution(MemoryResolutionError::Pool(source)).into()
}
}
impl From<RuntimeBootstrapError<MemoryBootstrapAdmissionError>> for DatabaseBootstrapError {
fn from(source: RuntimeBootstrapError<MemoryBootstrapAdmissionError>) -> Self {
Self::Bootstrap(Arc::new(source))
}
}
impl fmt::Display for DatabaseBootstrapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Construction(source) => source.fmt(f),
Self::Binding(source) => source.fmt(f),
Self::Bootstrap(source) => source.fmt(f),
Self::Adoption(source) => source.fmt(f),
}
}
}
impl std::error::Error for DatabaseBootstrapError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Construction(source) => Some(source.as_ref()),
Self::Binding(source) => Some(source),
Self::Bootstrap(source) => Some(source.as_ref()),
Self::Adoption(source) => Some(source.as_ref()),
}
}
}
#[cfg(test)]
mod tests {
mod public_failures;
use super::*;
use ic_memory::VectorMemory;
use ic_memory::{MemoryRequest, RuntimeOpenError};
const TEST_AUTHORITY: &str = "icydb.bootstrap_adoption_test";
fn pool() -> Result<MemoryAllocationPool, MemoryAllocationPoolError> {
MemoryAllocationPool::new(
vec![ic_memory::MemoryAuthority::new(
TEST_AUTHORITY,
"icydb.bootstrap_adoption_test.",
)?],
vec![],
)
}
struct ExistingRuntimePolicy {
preparation_calls: std::cell::Cell<usize>,
}
impl RuntimeBootstrapPolicy for ExistingRuntimePolicy {
type Error = MemoryBootstrapAdmissionError;
fn validate_allocation(
&self,
_: &StableKey,
_: &MemoryManagerSlot,
) -> Result<(), Self::Error> {
Ok(())
}
fn prepare_bootstrap(
&self,
admission: &mut BootstrapAdmission<'_>,
) -> Result<(), Self::Error> {
self.preparation_calls.set(self.preparation_calls.get() + 1);
prepare_memory_bootstrap(admission)
}
fn runtime_bootstrap_identity(&self) -> PolicyIdentity {
PolicyIdentity::new([7; 32])
}
}
fn requirements() -> SealedDeclarationSnapshot {
SealedDeclarationSnapshot::new(
["commit.control", "startup.control", "integrity.progress"]
.into_iter()
.map(|role| {
MemoryRequest::new(TEST_AUTHORITY, &format!("{TEST_AUTHORITY}.{role}.v1"))
.unwrap()
})
.collect(),
)
.unwrap()
}
#[test]
fn configured_bootstrap_selects_fresh_buckets_and_is_idempotent() {
for pages in [4, 16, 128] {
std::thread::spawn(move || {
assert!(memory_runtime().is_none());
assert!(matches!(
icydb_core::db::open_memory("icydb.bootstrap_adoption_test.commit.control.v1"),
Err(RuntimeOpenError::NotBootstrapped)
));
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), pages, pool)
.unwrap();
let runtime = memory_runtime().unwrap();
let committed = runtime.committed_allocations().unwrap().to_vec();
let before = runtime.memory_allocation_summary().unwrap();
assert_eq!(before.bucket_size_pages, pages);
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), pages, pool)
.unwrap();
assert_eq!(runtime.committed_allocations().unwrap(), &committed);
assert_eq!(runtime.memory_allocation_summary().unwrap(), before);
})
.join()
.unwrap();
}
}
#[test]
fn adopts_runtime_bound_during_pool_callback_before_constructing_memory() {
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), 4, || {
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), 16, pool).unwrap();
pool()
})
.unwrap();
let runtime = memory_runtime().unwrap();
assert_eq!(
runtime
.memory_allocation_summary()
.unwrap()
.bucket_size_pages,
16
);
assert_eq!(runtime.committed_generation().unwrap(), 1);
}
#[test]
fn conflicting_persisted_layout_rejects_without_changing_allocation() {
let backing = VectorMemory::default();
let mut runtime =
MemoryRuntime::new_with_config(backing.clone(), MemoryManagerConfig::new(128).unwrap())
.unwrap();
runtime
.bootstrap(&requirements(), &pool().unwrap(), &DatabaseMemoryPolicy)
.unwrap();
drop(runtime);
let before = backing.borrow().clone();
let error =
MemoryRuntime::new_with_config(backing.clone(), MemoryManagerConfig::new(16).unwrap())
.err()
.unwrap();
assert!(matches!(
error,
RuntimeConstructionError::BucketSizeMismatch {
persisted: 128,
requested: 16
}
));
let public = crate::db::startup::__startup_bootstrap_failure(error.into());
assert_eq!(
public.error().code(),
icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_MEMORY_BUCKET_SIZE_MISMATCH
);
assert_eq!(
public.error().core_facts().unwrap(),
vec![
(icydb_diagnostic_code::DiagnosticFactTag::Expected, 16),
(icydb_diagnostic_code::DiagnosticFactTag::Actual, 128),
]
);
assert_eq!(*backing.borrow(), before);
assert!(
MemoryRuntime::new_with_config(backing, MemoryManagerConfig::new(128).unwrap()).is_ok()
);
}
#[test]
fn unknown_host_authority_rejects_without_changing_committed_allocations() {
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), 16, pool).unwrap();
let runtime = memory_runtime().unwrap();
let committed = runtime.committed_allocations().unwrap().to_vec();
let before = runtime.memory_allocation_summary().unwrap();
let error =
ensure_default_memory_manager("icydb.unknown", &requirements(), 4, pool).unwrap_err();
let public = crate::Error::from(error.clone());
assert_eq!(
public.code(),
icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_MEMORY_DECLARATION_SNAPSHOT_MISMATCH
);
assert!(public.facts().is_empty());
assert_eq!(
crate::db::__startup_bootstrap_failure(error.clone()).error(),
&public
);
assert!(matches!(error, DatabaseBootstrapError::Adoption(cause)
if matches!(cause.as_ref(), RuntimeAdoptionError::UnknownAuthority { authority }
if authority == "icydb.unknown")));
assert_eq!(runtime.committed_allocations().unwrap(), &committed);
assert_eq!(runtime.memory_allocation_summary().unwrap(), before);
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), 4, pool).unwrap();
}
#[test]
fn adopts_runtime_bootstrapped_by_a_different_policy_and_bucket_size() {
let policy = ExistingRuntimePolicy {
preparation_calls: std::cell::Cell::new(0),
};
let mut runtime = MemoryRuntime::new_with_config(
DefaultMemoryImpl::default(),
MemoryManagerConfig::new(16).unwrap(),
)
.unwrap();
let declarations = requirements();
let host_pool = pool().unwrap();
let generation = runtime
.bootstrap(&declarations, &host_pool, &policy)
.unwrap();
assert_eq!(policy.preparation_calls.get(), 1);
assert_eq!(
runtime
.bootstrap(&declarations, &host_pool, &policy)
.unwrap(),
generation
);
assert_eq!(policy.preparation_calls.get(), 1);
let runtime = Rc::new(runtime);
bind_memory_runtime(Rc::clone(&runtime)).unwrap();
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), 4, || {
panic!("adoption must not invoke pool provider")
})
.unwrap();
ensure_default_memory_manager(TEST_AUTHORITY, &requirements(), 4, pool).unwrap();
assert_eq!(policy.preparation_calls.get(), 1);
assert_eq!(runtime.committed_generation().unwrap(), generation);
assert_eq!(
runtime
.memory_allocation_summary()
.unwrap()
.bucket_size_pages,
16
);
assert!(Rc::ptr_eq(&memory_runtime().unwrap(), &runtime));
}
}