icydb-core 0.157.21

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: commit::memory
//! Responsibility: resolve and validate the commit-marker stable-memory slot.
//! Does not own: marker encoding, marker persistence, or recovery orchestration.
//! Boundary: commit::{recovery,store} -> commit::memory (one-way).

#[cfg(not(test))]
use crate::db::commit::marker::COMMIT_LABEL;
use crate::error::InternalError;
use canic_cdk::structures::{DefaultMemoryImpl, memory::VirtualMemory};
use canic_memory::api::{MemoryApi, MemoryInspection};
use std::sync::OnceLock;

static COMMIT_STORE_ALLOCATION: OnceLock<CommitMemoryAllocation> = OnceLock::new();

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct CommitMemoryAllocation {
    memory_id: u8,
    stable_key: &'static str,
}

fn commit_memory_allocation() -> Result<CommitMemoryAllocation, InternalError> {
    COMMIT_STORE_ALLOCATION
        .get()
        .copied()
        .ok_or_else(InternalError::commit_memory_id_unconfigured)
}

/// Configure and register the commit marker memory id.
pub(in crate::db::commit) fn configure_commit_memory_id(
    memory_id: u8,
    stable_key: &'static str,
) -> Result<u8, InternalError> {
    // Phase 1: enforce one immutable runtime slot id per process.
    if let Some(cached_id) = validate_cached_commit_memory_allocation(memory_id, stable_key)? {
        return Ok(cached_id);
    }

    #[cfg(test)]
    {
        let _ = COMMIT_STORE_ALLOCATION.set(CommitMemoryAllocation {
            memory_id,
            stable_key,
        });
        Ok(memory_id)
    }

    #[cfg(not(test))]
    {
        // Phase 2: flush deferred registry state and validate slot ownership.
        MemoryApi::bootstrap_pending()
            .map_err(InternalError::commit_memory_registry_init_failed)?;

        validate_commit_slot_registration(memory_id, stable_key)?;

        // Phase 3: cache the validated slot id for all future accesses.
        let _ = COMMIT_STORE_ALLOCATION.set(CommitMemoryAllocation {
            memory_id,
            stable_key,
        });
        Ok(memory_id)
    }
}

fn validate_cached_commit_memory_allocation(
    memory_id: u8,
    stable_key: &'static str,
) -> Result<Option<u8>, InternalError> {
    let Some(cached) = COMMIT_STORE_ALLOCATION.get() else {
        return Ok(None);
    };

    if cached.memory_id != memory_id {
        return Err(InternalError::commit_memory_id_mismatch(
            cached.memory_id,
            memory_id,
        ));
    }
    if cached.stable_key != stable_key {
        return Err(InternalError::commit_memory_stable_key_mismatch(
            cached.stable_key,
            stable_key,
        ));
    }

    Ok(Some(cached.memory_id))
}

/// Open the configured commit-marker memory slot through the shared memory API.
pub(super) fn commit_memory_handle() -> Result<VirtualMemory<DefaultMemoryImpl>, InternalError> {
    let allocation = commit_memory_allocation()?;

    #[cfg(test)]
    {
        Ok(crate::testing::test_memory(allocation.memory_id))
    }

    #[cfg(not(test))]
    {
        let owner = owner_for_memory_id(allocation.memory_id)?;

        MemoryApi::register_with_key(
            allocation.memory_id,
            &owner,
            COMMIT_LABEL,
            allocation.stable_key,
        )
        .map_err(InternalError::commit_memory_id_registration_failed)
    }
}

/// Validate that exactly one canonical commit-marker slot exists.
#[cfg(not(test))]
fn validate_commit_slot_registration(
    memory_id: u8,
    stable_key: &'static str,
) -> Result<(), InternalError> {
    let mut commit_ids = MemoryApi::registered()
        .into_iter()
        .filter_map(|entry| (entry.stable_key == stable_key).then_some(entry.id))
        .collect::<Vec<_>>();
    commit_ids.sort_unstable();
    commit_ids.dedup();

    match commit_ids.as_slice() {
        [] => Err(InternalError::commit_memory_stable_key_unregistered(
            memory_id, stable_key,
        )),
        [registered_id] if *registered_id == memory_id => Ok(()),
        [registered_id] => Err(InternalError::configured_commit_memory_id_mismatch(
            memory_id,
            *registered_id,
        )),
        _ => Err(InternalError::multiple_commit_memory_ids_registered(
            commit_ids,
        )),
    }
}

/// Resolve the canonical owner label for one configured memory id.
#[cfg(not(test))]
fn owner_for_memory_id(memory_id: u8) -> Result<String, InternalError> {
    Ok(inspect_memory(memory_id)?.owner)
}

// Inspect one configured memory id through the public Canic memory API.
#[cfg(not(test))]
fn inspect_memory(memory_id: u8) -> Result<MemoryInspection, InternalError> {
    MemoryApi::inspect(memory_id)
        .ok_or_else(|| InternalError::commit_memory_id_outside_reserved_ranges(memory_id))
}

///
/// TESTS
///

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::{ErrorClass, ErrorOrigin};

    #[test]
    fn owner_for_memory_inspection_returns_matching_owner() {
        let inspection = MemoryInspection {
            id: 12,
            owner: "b".to_string(),
            range: canic_memory::registry::MemoryRange { start: 11, end: 20 },
            label: None,
            stable_key: None,
            schema_version: None,
            schema_fingerprint: None,
        };
        let owner = inspection.owner;
        assert_eq!(owner, "b");
    }

    #[test]
    fn inspect_memory_missing_id_maps_to_out_of_range_error() {
        let err = MemoryApi::inspect(30)
            .ok_or_else(|| InternalError::commit_memory_id_outside_reserved_ranges(30))
            .expect_err("id outside ranges must fail");
        assert_eq!(err.class, ErrorClass::Unsupported);
        assert_eq!(err.origin, ErrorOrigin::Store);
    }
}