#[cfg(not(test))]
use crate::db::registry::open_committed_memory;
use crate::error::InternalError;
use ic_memory::{DefaultMemoryImpl, RuntimeMemory};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
thread_local! {
static CURRENT_COMMIT_STORE_ALLOCATION: Cell<Option<CommitMemoryAllocation>> =
const { Cell::new(None) };
static COMMIT_MEMORIES: RefCell<
Vec<(CommitMemoryAllocation, Rc<RuntimeMemory<DefaultMemoryImpl>>)>
> = const { RefCell::new(Vec::new()) };
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct CommitMemoryAllocation {
pub(in crate::db) memory_id: u8,
pub(in crate::db) stable_key: &'static str,
}
pub(in crate::db) fn current_commit_memory_allocation()
-> Result<CommitMemoryAllocation, InternalError> {
CURRENT_COMMIT_STORE_ALLOCATION.with(|cell| {
cell.get()
.ok_or_else(InternalError::commit_memory_id_unconfigured)
})
}
pub(in crate::db) fn current_commit_memory_allocation_if_configured()
-> Option<CommitMemoryAllocation> {
CURRENT_COMMIT_STORE_ALLOCATION.with(Cell::get)
}
pub(in crate::db) fn select_commit_memory_allocation(memory_id: u8, stable_key: &'static str) {
let allocation = CommitMemoryAllocation {
memory_id,
stable_key,
};
CURRENT_COMMIT_STORE_ALLOCATION.with(|cell| cell.set(Some(allocation)));
}
pub(in crate::db) fn commit_memory_handle(
allocation: CommitMemoryAllocation,
) -> Result<Rc<RuntimeMemory<DefaultMemoryImpl>>, InternalError> {
COMMIT_MEMORIES.with(|memories| {
let mut memories = memories
.try_borrow_mut()
.map_err(|_| InternalError::store_invariant())?;
if let Some((_, memory)) = memories
.iter()
.find(|(existing, _)| *existing == allocation)
{
return Ok(Rc::clone(memory));
}
#[cfg(test)]
let memory = crate::testing::test_memory(allocation.memory_id);
#[cfg(not(test))]
let memory = open_committed_memory(allocation.stable_key, allocation.memory_id)?;
let memory = Rc::new(memory);
memories.push((allocation, Rc::clone(&memory)));
Ok(memory)
})
}
#[doc(hidden)]
pub fn with_selected_commit_memory<R>(
stable_key: &str,
access: impl FnOnce(&RuntimeMemory<DefaultMemoryImpl>) -> R,
) -> Result<R, InternalError> {
let allocation = current_commit_memory_allocation()?;
if allocation.stable_key != stable_key {
return Err(InternalError::store_internal());
}
let memory = commit_memory_handle(allocation)?;
Ok(access(&memory))
}
#[cfg(test)]
mod tests {
use super::*;
use ic_memory::Memory as _;
#[test]
fn current_commit_selection_can_switch_databases_and_return() {
let first = CommitMemoryAllocation {
memory_id: 12,
stable_key: "icydb.test.commit.control.v1",
};
let second = CommitMemoryAllocation {
memory_id: 30,
stable_key: "icydb.test.commit.peer-control.v1",
};
for allocation in [first, first, second, first] {
select_commit_memory_allocation(allocation.memory_id, allocation.stable_key);
assert_eq!(current_commit_memory_allocation().unwrap(), allocation);
}
}
#[test]
fn selected_control_borrow_keeps_the_live_lease_and_refuses_another_key() {
const KEY: &str = "icydb.test.borrowed.commit.control.v1";
select_commit_memory_allocation(93, KEY);
let owner = commit_memory_handle(current_commit_memory_allocation().unwrap()).unwrap();
owner.as_ref().grow(1).unwrap();
owner.write(0, b"retained");
let bytes = with_selected_commit_memory(KEY, |memory| {
let mut bytes = [0; 8];
memory.read(0, &mut bytes);
bytes
})
.unwrap();
assert_eq!(&bytes, b"retained");
let called = Cell::new(false);
assert!(
with_selected_commit_memory("icydb.test.other.commit.control.v1", |_| {
called.set(true);
})
.is_err()
);
assert!(!called.get());
let mut retained = [0; 8];
owner.read(0, &mut retained);
assert_eq!(&retained, b"retained");
}
}