use std::{
ops::Deref,
panic::{AssertUnwindSafe, catch_unwind},
sync::{
Condvar, Mutex, MutexGuard, TryLockError,
atomic::{AtomicUsize, Ordering},
},
};
use super::{
ControllerSnapshotError, ControllerSnapshots, PocketIcSnapshotExt, SnapshotRestoreFunding,
StandaloneCanisterFixture, 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: [Mutex<Option<StandaloneFixtureBaseline>>; CAPACITY],
next_slot: AtomicUsize,
wait_lock: Mutex<()>,
slot_released: Condvar,
restore_funding: SnapshotRestoreFunding,
}
pub struct CachedStandaloneCanisterFixtureGuard<'a> {
slot: Option<MutexGuard<'a, Option<StandaloneFixtureBaseline>>>,
wait_lock: &'a Mutex<()>,
slot_released: &'a Condvar,
}
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: [const { Mutex::new(None) }; CAPACITY],
next_slot: AtomicUsize::new(0),
wait_lock: Mutex::new(()),
slot_released: Condvar::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,
{
let mut start = self.next_slot.fetch_add(1, Ordering::Relaxed) % CAPACITY;
loop {
if let Some(slot) = self.try_acquire_slot(start) {
return self.prepare_slot(slot, &build);
}
let wait_guard = self
.wait_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(slot) = self.try_acquire_slot(start) {
drop(wait_guard);
return self.prepare_slot(slot, &build);
}
drop(
self.slot_released
.wait(wait_guard)
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
start = self.next_slot.fetch_add(1, Ordering::Relaxed) % CAPACITY;
}
}
fn try_acquire_slot(
&self,
start: usize,
) -> Option<MutexGuard<'_, Option<StandaloneFixtureBaseline>>> {
for offset in 0..CAPACITY {
let slot_index = (start + offset) % CAPACITY;
match self.slots[slot_index].try_lock() {
Ok(slot) => return Some(slot),
Err(TryLockError::Poisoned(error)) => return Some(error.into_inner()),
Err(TryLockError::WouldBlock) => {}
}
}
None
}
fn prepare_slot<'a, B>(
&'a self,
slot: MutexGuard<'a, Option<StandaloneFixtureBaseline>>,
build: &B,
) -> Result<(CachedStandaloneCanisterFixtureGuard<'a>, bool), ControllerSnapshotError>
where
B: Fn() -> StandaloneCanisterFixture,
{
let mut guard = self.guard(slot);
let slot = guard
.slot
.as_mut()
.expect("fixture pool reservation must retain its slot");
let cache_hit = slot.is_some();
if !cache_hit {
**slot = Some(StandaloneFixtureBaseline::capture(build())?);
return Ok((guard, false));
}
let restore = slot
.as_ref()
.expect("populated fixture pool slot must remain present")
.restore(self.restore_funding);
match restore {
Ok(()) => Ok((guard, 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 = Some(StandaloneFixtureBaseline::capture(build())?);
Ok((guard, false))
}
Err(error) => Err(error),
}
}
const fn guard<'a>(
&'a self,
slot: MutexGuard<'a, Option<StandaloneFixtureBaseline>>,
) -> CachedStandaloneCanisterFixtureGuard<'a> {
CachedStandaloneCanisterFixtureGuard {
slot: Some(slot),
wait_lock: &self.wait_lock,
slot_released: &self.slot_released,
}
}
}
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
.as_ref()
.expect("fixture pool guard must retain its slot")
.as_ref()
.expect("leased fixture pool slot must remain populated")
.fixture
}
}
impl Drop for CachedStandaloneCanisterFixtureGuard<'_> {
fn drop(&mut self) {
drop(self.slot.take());
let wait_guard = self
.wait_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.slot_released.notify_one();
drop(wait_guard);
}
}
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();
}
}