use std::{
num::NonZeroUsize,
ops::Deref,
panic::{AssertUnwindSafe, catch_unwind},
sync::OnceLock,
};
use super::{
ControllerSnapshotError, ControllerSnapshots, PocketIcSnapshotExt, SnapshotRestoreFunding,
StandaloneCanisterFixture,
bounded_pool::{BoundedSlotLease, BoundedSlotPool},
transport,
};
struct StandaloneFixtureBaseline {
fixture: StandaloneCanisterFixture,
snapshots: ControllerSnapshots,
}
impl StandaloneFixtureBaseline {
fn capture(fixture: StandaloneCanisterFixture) -> Result<Self, ControllerSnapshotError> {
let canister_id = fixture.canister_id();
let snapshots = fixture
.pocket_ic()
.capture_controller_snapshots(canister_id, [canister_id])?;
Ok(Self { fixture, snapshots })
}
fn restore(&self, funding: SnapshotRestoreFunding) -> Result<(), ControllerSnapshotError> {
self.fixture
.pocket_ic()
.restore_controller_snapshots_with_funding(
self.fixture.canister_id(),
&self.snapshots,
funding,
)
}
}
pub struct CachedStandaloneCanisterFixturePool<const CAPACITY: usize> {
slots: OnceLock<BoundedSlotPool<StandaloneFixtureBaseline>>,
restore_funding: SnapshotRestoreFunding,
}
pub struct CachedStandaloneCanisterFixtureGuard<'a> {
slot: BoundedSlotLease<'a, StandaloneFixtureBaseline>,
}
impl<const CAPACITY: usize> CachedStandaloneCanisterFixturePool<CAPACITY> {
#[must_use]
pub const fn new() -> Self {
assert!(CAPACITY > 0, "fixture pool capacity must be non-zero");
Self {
slots: OnceLock::new(),
restore_funding: SnapshotRestoreFunding::Preserve,
}
}
#[must_use]
pub const fn with_restore_funding(mut self, funding: SnapshotRestoreFunding) -> Self {
self.restore_funding = funding;
self
}
pub fn acquire<B>(
&self,
build: B,
) -> Result<(CachedStandaloneCanisterFixtureGuard<'_>, bool), ControllerSnapshotError>
where
B: Fn() -> StandaloneCanisterFixture,
{
self.prepare_slot(self.slots().acquire(), &build)
}
fn prepare_slot<'a, B>(
&'a self,
mut slot: BoundedSlotLease<'a, StandaloneFixtureBaseline>,
build: &B,
) -> Result<(CachedStandaloneCanisterFixtureGuard<'a>, bool), ControllerSnapshotError>
where
B: Fn() -> StandaloneCanisterFixture,
{
if !slot.is_reusable() {
if let Some(stale) = slot.take() {
let _ = catch_unwind(AssertUnwindSafe(|| drop(stale)));
}
slot.replace(StandaloneFixtureBaseline::capture(build())?);
return Ok((CachedStandaloneCanisterFixtureGuard { slot }, false));
}
let restore = slot
.get()
.expect("populated fixture pool slot must remain present")
.restore(self.restore_funding);
match restore {
Ok(()) => Ok((CachedStandaloneCanisterFixtureGuard { slot }, true)),
Err(error) if snapshot_error_is_dead_instance_transport(&error) => {
let stale = slot.take();
if let Some(stale) = stale {
let _ = catch_unwind(AssertUnwindSafe(|| drop(stale)));
}
slot.replace(StandaloneFixtureBaseline::capture(build())?);
Ok((CachedStandaloneCanisterFixtureGuard { slot }, false))
}
Err(error) => {
slot.invalidate();
Err(error)
}
}
}
fn slots(&self) -> &BoundedSlotPool<StandaloneFixtureBaseline> {
self.slots.get_or_init(|| {
BoundedSlotPool::new(
NonZeroUsize::new(CAPACITY).expect("fixture pool capacity must be non-zero"),
)
})
}
}
impl<const CAPACITY: usize> Default for CachedStandaloneCanisterFixturePool<CAPACITY> {
fn default() -> Self {
Self::new()
}
}
impl Deref for CachedStandaloneCanisterFixtureGuard<'_> {
type Target = StandaloneCanisterFixture;
fn deref(&self) -> &Self::Target {
&self
.slot
.get()
.expect("leased fixture pool slot must remain populated")
.fixture
}
}
fn snapshot_error_is_dead_instance_transport(error: &ControllerSnapshotError) -> bool {
matches!(
error,
ControllerSnapshotError::RestorePanicked { message, .. }
if transport::is_dead_instance_transport_error(message)
)
}
#[cfg(test)]
mod tests {
use super::CachedStandaloneCanisterFixturePool;
const _: CachedStandaloneCanisterFixturePool<1> = CachedStandaloneCanisterFixturePool::new();
#[test]
fn nonzero_pool_constructs() {
let _pool = CachedStandaloneCanisterFixturePool::<2>::new();
}
}