use super::RuntimeConstructionError;
use ic_platform::stable_structures::{Memory, VectorMemory};
use std::{collections::BTreeSet, rc::Rc, sync::Arc};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct BackingIdentity(usize);
impl BackingIdentity {
#[must_use]
pub fn of_rc<T: ?Sized>(backing: &Rc<T>) -> Self {
Self(Rc::as_ptr(backing).cast::<()>() as usize)
}
#[must_use]
pub fn of_arc<T: ?Sized>(backing: &Arc<T>) -> Self {
Self(Arc::as_ptr(backing).cast::<()>() as usize)
}
}
pub trait MemoryBacking: Memory {
fn backing_identity(&self) -> BackingIdentity;
}
impl MemoryBacking for VectorMemory {
fn backing_identity(&self) -> BackingIdentity {
BackingIdentity::of_rc(self)
}
}
impl<M: MemoryBacking> MemoryBacking for Rc<M> {
fn backing_identity(&self) -> BackingIdentity {
self.as_ref().backing_identity()
}
}
#[cfg(target_arch = "wasm32")]
impl MemoryBacking for ic_platform::stable_structures::DefaultMemoryImpl {
fn backing_identity(&self) -> BackingIdentity {
BackingIdentity(0)
}
}
#[cfg(not(target_arch = "wasm32"))]
static CLAIMS: std::sync::Mutex<BTreeSet<BackingIdentity>> = std::sync::Mutex::new(BTreeSet::new());
#[cfg(target_arch = "wasm32")]
thread_local! {
static CLAIMS: std::cell::RefCell<BTreeSet<BackingIdentity>> = const {
std::cell::RefCell::new(BTreeSet::new())
};
}
fn with_claims<T>(operation: impl FnOnce(&mut BTreeSet<BackingIdentity>) -> T) -> T {
#[cfg(not(target_arch = "wasm32"))]
{
let mut claims = CLAIMS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
operation(&mut claims)
}
#[cfg(target_arch = "wasm32")]
CLAIMS.with(|claims| operation(&mut claims.borrow_mut()))
}
pub(super) struct BackingClaim(BackingIdentity);
impl BackingClaim {
pub fn acquire(memory: &impl MemoryBacking) -> Result<Self, RuntimeConstructionError> {
let identity = memory.backing_identity();
if with_claims(|claims| claims.insert(identity)) {
Ok(Self(identity))
} else {
Err(RuntimeConstructionError::BackingAlreadyClaimed)
}
}
}
impl Drop for BackingClaim {
fn drop(&mut self) {
with_claims(|claims| claims.remove(&self.0));
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
impl<M: MemoryBacking> MemoryBacking for ic_testkit_native::memory::MeteredMemory<M> {
fn backing_identity(&self) -> BackingIdentity {
self.backing().backing_identity()
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use crate::{MemoryManagerConfig, MemoryRuntime, testing::fixture_runtime};
use std::{cell::Cell, panic::AssertUnwindSafe};
#[derive(Clone, Copy, Debug)]
enum Failure {
None,
Grow,
Initialize,
Capture,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Destruction {
strong_refs: usize,
claimed_after_free: bool,
}
struct TrackedBacking {
memory: Option<VectorMemory>,
failure: Failure,
destruction: Rc<Cell<Option<Destruction>>>,
}
impl TrackedBacking {
fn new(failure: Failure) -> Self {
Self {
memory: Some(VectorMemory::default()),
failure,
destruction: Rc::default(),
}
}
const fn memory(&self) -> &VectorMemory {
self.memory.as_ref().unwrap()
}
}
impl MemoryBacking for TrackedBacking {
fn backing_identity(&self) -> BackingIdentity {
self.memory().backing_identity()
}
}
impl Memory for TrackedBacking {
fn size(&self) -> u64 {
self.memory().size()
}
fn grow(&self, pages: u64) -> i64 {
if matches!(self.failure, Failure::Grow) {
-1
} else {
self.memory().grow(pages)
}
}
fn read(&self, offset: u64, dst: &mut [u8]) {
assert!(
!matches!(self.failure, Failure::Capture)
|| dst.len() != super::super::layout::METADATA_BYTES,
"injected authority capture failure"
);
self.memory().read(offset, dst);
}
fn write(&self, offset: u64, src: &[u8]) {
assert!(
!matches!(self.failure, Failure::Initialize),
"injected manager initialization failure"
);
self.memory().write(offset, src);
}
}
impl Drop for TrackedBacking {
fn drop(&mut self) {
let memory = self.memory.take().unwrap();
let identity = memory.backing_identity();
let strong_refs = Rc::strong_count(&memory);
let claimed_after_free = with_claims(|claims| {
drop(memory);
claims.contains(&identity)
});
self.destruction.set(Some(Destruction {
strong_refs,
claimed_after_free,
}));
}
}
#[test]
fn final_runtime_or_detached_handle_releases_claim_before_identity_allocation() {
for detach_handle in [false, true] {
let backing = TrackedBacking::new(Failure::None);
let destruction = Rc::clone(&backing.destruction);
let identity = backing.backing_identity();
let runtime = fixture_runtime(
backing,
"drop",
"drop.",
&["drop.rows.v1"],
MemoryManagerConfig::new(8).unwrap(),
)
.unwrap();
let handle = detach_handle.then(|| runtime.open_memory("drop.rows.v1").unwrap());
drop(runtime);
if handle.is_some() {
assert_eq!(destruction.get(), None);
assert!(with_claims(|claims| claims.contains(&identity)));
}
drop(handle);
assert_eq!(
destruction.get(),
Some(Destruction {
strong_refs: 1,
claimed_after_free: false,
})
);
}
}
#[test]
fn construction_refusal_and_unwind_release_claim_before_identity_allocation() {
for failure in [Failure::Grow, Failure::Initialize, Failure::Capture] {
let backing = TrackedBacking::new(failure);
let destruction = Rc::clone(&backing.destruction);
let result = std::panic::catch_unwind(AssertUnwindSafe(|| MemoryRuntime::new(backing)));
if matches!(failure, Failure::Grow) {
assert!(matches!(
result,
Ok(Err(RuntimeConstructionError::Growth(_)))
));
} else {
assert!(result.is_err(), "expected injected {failure:?} failure");
}
assert_eq!(
destruction.get(),
Some(Destruction {
strong_refs: 1,
claimed_after_free: false,
}),
"failure during {failure:?} retained a dead backing identity"
);
}
}
#[test]
fn independent_backings_construct_and_drop_concurrently() {
let start = std::sync::Barrier::new(4);
std::thread::scope(|scope| {
for _ in 0..4 {
let start = &start;
scope.spawn(move || {
start.wait();
for _ in 0..2000 {
drop(MemoryRuntime::new(VectorMemory::default()).unwrap());
}
});
}
});
}
}