ic_testkit/pic/
standalone_pool.rs1use std::{
2 ops::Deref,
3 panic::{AssertUnwindSafe, catch_unwind},
4 sync::{
5 Condvar, Mutex, MutexGuard, TryLockError,
6 atomic::{AtomicUsize, Ordering},
7 },
8};
9
10use super::{
11 ControllerSnapshotError, ControllerSnapshots, PocketIcSnapshotExt, SnapshotRestoreFunding,
12 StandaloneCanisterFixture, 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: [Mutex<Option<StandaloneFixtureBaseline>>; CAPACITY],
63 next_slot: AtomicUsize,
64 wait_lock: Mutex<()>,
65 slot_released: Condvar,
66 restore_funding: SnapshotRestoreFunding,
67}
68
69pub struct CachedStandaloneCanisterFixtureGuard<'a> {
74 slot: Option<MutexGuard<'a, Option<StandaloneFixtureBaseline>>>,
75 wait_lock: &'a Mutex<()>,
76 slot_released: &'a Condvar,
77}
78
79impl<const CAPACITY: usize> CachedStandaloneCanisterFixturePool<CAPACITY> {
80 #[must_use]
87 pub const fn new() -> Self {
88 assert!(CAPACITY > 0, "fixture pool capacity must be non-zero");
89
90 Self {
91 slots: [const { Mutex::new(None) }; CAPACITY],
92 next_slot: AtomicUsize::new(0),
93 wait_lock: Mutex::new(()),
94 slot_released: Condvar::new(),
95 restore_funding: SnapshotRestoreFunding::Preserve,
96 }
97 }
98
99 #[must_use]
102 pub const fn with_restore_funding(mut self, funding: SnapshotRestoreFunding) -> Self {
103 self.restore_funding = funding;
104 self
105 }
106
107 pub fn acquire<B>(
122 &self,
123 build: B,
124 ) -> Result<(CachedStandaloneCanisterFixtureGuard<'_>, bool), ControllerSnapshotError>
125 where
126 B: Fn() -> StandaloneCanisterFixture,
127 {
128 let mut start = self.next_slot.fetch_add(1, Ordering::Relaxed) % CAPACITY;
129
130 loop {
131 if let Some(slot) = self.try_acquire_slot(start) {
132 return self.prepare_slot(slot, &build);
133 }
134
135 let wait_guard = self
136 .wait_lock
137 .lock()
138 .unwrap_or_else(std::sync::PoisonError::into_inner);
139 if let Some(slot) = self.try_acquire_slot(start) {
140 drop(wait_guard);
141 return self.prepare_slot(slot, &build);
142 }
143
144 drop(
145 self.slot_released
146 .wait(wait_guard)
147 .unwrap_or_else(std::sync::PoisonError::into_inner),
148 );
149 start = self.next_slot.fetch_add(1, Ordering::Relaxed) % CAPACITY;
150 }
151 }
152
153 fn try_acquire_slot(
154 &self,
155 start: usize,
156 ) -> Option<MutexGuard<'_, Option<StandaloneFixtureBaseline>>> {
157 for offset in 0..CAPACITY {
158 let slot_index = (start + offset) % CAPACITY;
159 match self.slots[slot_index].try_lock() {
160 Ok(slot) => return Some(slot),
161 Err(TryLockError::Poisoned(error)) => return Some(error.into_inner()),
162 Err(TryLockError::WouldBlock) => {}
163 }
164 }
165
166 None
167 }
168
169 fn prepare_slot<'a, B>(
170 &'a self,
171 slot: MutexGuard<'a, Option<StandaloneFixtureBaseline>>,
172 build: &B,
173 ) -> Result<(CachedStandaloneCanisterFixtureGuard<'a>, bool), ControllerSnapshotError>
174 where
175 B: Fn() -> StandaloneCanisterFixture,
176 {
177 let mut guard = self.guard(slot);
181 let slot = guard
182 .slot
183 .as_mut()
184 .expect("fixture pool reservation must retain its slot");
185 let cache_hit = slot.is_some();
186 if !cache_hit {
187 **slot = Some(StandaloneFixtureBaseline::capture(build())?);
188 return Ok((guard, false));
189 }
190
191 let restore = slot
192 .as_ref()
193 .expect("populated fixture pool slot must remain present")
194 .restore(self.restore_funding);
195 match restore {
196 Ok(()) => Ok((guard, true)),
197 Err(error) if snapshot_error_is_dead_instance_transport(&error) => {
198 let stale = slot.take();
199 if let Some(stale) = stale {
200 let _ = catch_unwind(AssertUnwindSafe(|| drop(stale)));
201 }
202 **slot = Some(StandaloneFixtureBaseline::capture(build())?);
203 Ok((guard, false))
204 }
205 Err(error) => Err(error),
206 }
207 }
208
209 const fn guard<'a>(
210 &'a self,
211 slot: MutexGuard<'a, Option<StandaloneFixtureBaseline>>,
212 ) -> CachedStandaloneCanisterFixtureGuard<'a> {
213 CachedStandaloneCanisterFixtureGuard {
214 slot: Some(slot),
215 wait_lock: &self.wait_lock,
216 slot_released: &self.slot_released,
217 }
218 }
219}
220
221impl<const CAPACITY: usize> Default for CachedStandaloneCanisterFixturePool<CAPACITY> {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl Deref for CachedStandaloneCanisterFixtureGuard<'_> {
228 type Target = StandaloneCanisterFixture;
229
230 fn deref(&self) -> &Self::Target {
231 &self
232 .slot
233 .as_ref()
234 .expect("fixture pool guard must retain its slot")
235 .as_ref()
236 .expect("leased fixture pool slot must remain populated")
237 .fixture
238 }
239}
240
241impl Drop for CachedStandaloneCanisterFixtureGuard<'_> {
242 fn drop(&mut self) {
243 drop(self.slot.take());
244
245 let wait_guard = self
249 .wait_lock
250 .lock()
251 .unwrap_or_else(std::sync::PoisonError::into_inner);
252 self.slot_released.notify_one();
253 drop(wait_guard);
254 }
255}
256
257fn snapshot_error_is_dead_instance_transport(error: &ControllerSnapshotError) -> bool {
258 matches!(
259 error,
260 ControllerSnapshotError::RestorePanicked { message, .. }
261 if transport::is_dead_instance_transport_error(message)
262 )
263}
264
265#[cfg(test)]
266mod tests {
267 use super::CachedStandaloneCanisterFixturePool;
268
269 const _: CachedStandaloneCanisterFixturePool<1> = CachedStandaloneCanisterFixturePool::new();
270
271 #[test]
272 fn nonzero_pool_constructs() {
273 let _pool = CachedStandaloneCanisterFixturePool::<2>::new();
274 }
275}