ic_testkit/pic/
standalone_pool.rs1use std::{
2 num::NonZeroUsize,
3 ops::Deref,
4 panic::{AssertUnwindSafe, catch_unwind},
5 sync::OnceLock,
6};
7
8use super::{
9 ControllerSnapshotError, ControllerSnapshots, PocketIcSnapshotExt, SnapshotRestoreFunding,
10 StandaloneCanisterFixture,
11 bounded_pool::{BoundedSlotLease, BoundedSlotPool},
12 transport,
13};
14
15struct StandaloneFixtureBaseline {
16 fixture: StandaloneCanisterFixture,
17 snapshots: ControllerSnapshots,
18}
19
20impl StandaloneFixtureBaseline {
21 fn capture(fixture: StandaloneCanisterFixture) -> Result<Self, ControllerSnapshotError> {
22 let canister_id = fixture.canister_id();
23 let snapshots = fixture
24 .pocket_ic()
25 .capture_controller_snapshots(canister_id, [canister_id])?;
26
27 Ok(Self { fixture, snapshots })
28 }
29
30 fn restore(&self, funding: SnapshotRestoreFunding) -> Result<(), ControllerSnapshotError> {
31 self.fixture
32 .pocket_ic()
33 .restore_controller_snapshots_with_funding(
34 self.fixture.canister_id(),
35 &self.snapshots,
36 funding,
37 )
38 }
39}
40
41pub struct CachedStandaloneCanisterFixturePool<const CAPACITY: usize> {
62 slots: OnceLock<BoundedSlotPool<StandaloneFixtureBaseline>>,
63 restore_funding: SnapshotRestoreFunding,
64}
65
66pub struct CachedStandaloneCanisterFixtureGuard<'a> {
71 slot: BoundedSlotLease<'a, StandaloneFixtureBaseline>,
72}
73
74impl<const CAPACITY: usize> CachedStandaloneCanisterFixturePool<CAPACITY> {
75 #[must_use]
82 pub const fn new() -> Self {
83 assert!(CAPACITY > 0, "fixture pool capacity must be non-zero");
84
85 Self {
86 slots: OnceLock::new(),
87 restore_funding: SnapshotRestoreFunding::Preserve,
88 }
89 }
90
91 #[must_use]
94 pub const fn with_restore_funding(mut self, funding: SnapshotRestoreFunding) -> Self {
95 self.restore_funding = funding;
96 self
97 }
98
99 pub fn acquire<B>(
115 &self,
116 build: B,
117 ) -> Result<(CachedStandaloneCanisterFixtureGuard<'_>, bool), ControllerSnapshotError>
118 where
119 B: Fn() -> StandaloneCanisterFixture,
120 {
121 self.prepare_slot(self.slots().acquire(), &build)
122 }
123
124 fn prepare_slot<'a, B>(
125 &'a self,
126 mut slot: BoundedSlotLease<'a, StandaloneFixtureBaseline>,
127 build: &B,
128 ) -> Result<(CachedStandaloneCanisterFixtureGuard<'a>, bool), ControllerSnapshotError>
129 where
130 B: Fn() -> StandaloneCanisterFixture,
131 {
132 if !slot.is_reusable() {
133 if let Some(stale) = slot.take() {
134 let _ = catch_unwind(AssertUnwindSafe(|| drop(stale)));
135 }
136 slot.replace(StandaloneFixtureBaseline::capture(build())?);
137 return Ok((CachedStandaloneCanisterFixtureGuard { slot }, false));
138 }
139
140 let restore = slot
141 .get()
142 .expect("populated fixture pool slot must remain present")
143 .restore(self.restore_funding);
144 match restore {
145 Ok(()) => Ok((CachedStandaloneCanisterFixtureGuard { slot }, true)),
146 Err(error) if snapshot_error_is_dead_instance_transport(&error) => {
147 let stale = slot.take();
148 if let Some(stale) = stale {
149 let _ = catch_unwind(AssertUnwindSafe(|| drop(stale)));
150 }
151 slot.replace(StandaloneFixtureBaseline::capture(build())?);
152 Ok((CachedStandaloneCanisterFixtureGuard { slot }, false))
153 }
154 Err(error) => {
155 slot.invalidate();
159 Err(error)
160 }
161 }
162 }
163
164 fn slots(&self) -> &BoundedSlotPool<StandaloneFixtureBaseline> {
165 self.slots.get_or_init(|| {
166 BoundedSlotPool::new(
167 NonZeroUsize::new(CAPACITY).expect("fixture pool capacity must be non-zero"),
168 )
169 })
170 }
171}
172
173impl<const CAPACITY: usize> Default for CachedStandaloneCanisterFixturePool<CAPACITY> {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179impl Deref for CachedStandaloneCanisterFixtureGuard<'_> {
180 type Target = StandaloneCanisterFixture;
181
182 fn deref(&self) -> &Self::Target {
183 &self
184 .slot
185 .get()
186 .expect("leased fixture pool slot must remain populated")
187 .fixture
188 }
189}
190
191fn snapshot_error_is_dead_instance_transport(error: &ControllerSnapshotError) -> bool {
192 matches!(
193 error,
194 ControllerSnapshotError::RestorePanicked { message, .. }
195 if transport::is_dead_instance_transport_error(message)
196 )
197}
198
199#[cfg(test)]
200mod tests {
201 use super::CachedStandaloneCanisterFixturePool;
202
203 const _: CachedStandaloneCanisterFixturePool<1> = CachedStandaloneCanisterFixturePool::new();
204
205 #[test]
206 fn nonzero_pool_constructs() {
207 let _pool = CachedStandaloneCanisterFixturePool::<2>::new();
208 }
209}