simu/resource/preemptive.rs
1// SPDX-FileCopyrightText: Copyright (c) Siemens 2026 contributed by Christoph Kuhmuench christoph.kuhmuench@gmail.com
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! `PreemptiveResource` — a priority resource whose in-use units can be
6//! *preempted* by a higher-priority request.
7//!
8//! ## Cooperative-at-yield semantics
9//!
10//! A discrete-event executor cannot forcibly unwind a process that is suspended
11//! on an unrelated future (e.g. a service `timeout`): the only lever it has is
12//! the process's `Waker`, and waking it merely re-polls the same `Pending`
13//! future. Truly preemptive cancellation would require unwinding arbitrary
14//! suspended stacks, which Rust's async model does not permit from the outside.
15//!
16//! `PreemptiveResource` therefore delivers preemption the way SimPy delivers an
17//! interrupt and the way all Rust async cancellation works: **at the victim's
18//! next yield point.** When a higher-priority request preempts a holder, the
19//! holder's unit is transferred away immediately *and* its preemption signal is
20//! fired. A well-behaved victim races its work against that signal — see the
21//! [`PreemptiveResource`] type docs for the full pattern.
22//!
23//! A victim that ignores its signal keeps running (it has already lost the unit
24//! on the books, so it can no longer block anyone). This mirrors the fact that
25//! a Rust future which never checks for cancellation simply runs to completion.
26
27use std::cell::{Cell, RefCell};
28use std::future::Future;
29use std::pin::Pin;
30use std::rc::Rc;
31use std::task::{Context, Poll};
32
33use crate::event::{new_event, EventAwaitable, EventTrigger};
34
35use super::wait_queue::WaitQueue;
36
37/// Bookkeeping for one unit that is currently held.
38struct Holder {
39 /// Priority the unit was acquired at (lower = higher priority). Used to
40 /// pick a victim: a request preempts the lowest-priority holder whose
41 /// priority is strictly worse (greater) than the request's.
42 priority: u32,
43 /// Unique id so a guard can find and remove exactly its own holder entry
44 /// (priorities are not unique).
45 id: u64,
46 /// Fired when this holder is preempted. Shared with the guard's
47 /// `EventAwaitable` so the victim can observe preemption at a yield point.
48 trigger: Option<EventTrigger>,
49 /// Mirrors `trigger`'s fired state for synchronous `is_preempted()` checks
50 /// without consuming the awaitable.
51 preempted: Rc<Cell<bool>>,
52}
53
54struct PreemptiveState {
55 /// Capacity accounting + the priority-ordered queue of *blocked* requests.
56 /// `in_use` here counts granted units; `holders.len()` mirrors it exactly.
57 wq: WaitQueue<u32>,
58 /// One entry per currently-held unit.
59 holders: Vec<Holder>,
60 /// Monotonic id source for holder entries.
61 next_holder_id: u64,
62}
63
64impl PreemptiveState {
65 /// Pick the holder to evict for an `incoming` request, or `None` if none
66 /// can be preempted.
67 ///
68 /// The victim is the holder with the **lowest priority** (numerically
69 /// greatest) whose priority is *strictly worse* than `incoming`. When
70 /// several holders share that lowest priority, the **most recently
71 /// acquired** one is chosen — it has made the least progress, so preempting
72 /// it wastes the least work. `max_by_key` returns the last maximal element,
73 /// and `holders` is kept in acquisition order, so this tie-break is
74 /// deterministic.
75 fn victim_index(&self, incoming: u32) -> Option<usize> {
76 self.holders
77 .iter()
78 .enumerate()
79 .filter(|(_, h)| h.priority > incoming)
80 .max_by_key(|(_, h)| h.priority)
81 .map(|(i, _)| i)
82 }
83}
84
85/// A cloneable handle to a capacity-limited pool whose held units can be
86/// **preempted** by higher-priority requests.
87///
88/// Like [`PriorityResource`](crate::PriorityResource), units are requested at a
89/// priority (lower number = higher priority) and blocked waiters are served in
90/// priority order. *Unlike* it, when every unit is in use a higher-priority
91/// request does not wait behind the holders — it **evicts** the lowest-priority
92/// holder whose priority is strictly worse than its own, taking that unit
93/// immediately.
94///
95/// Preemption is cooperative-at-yield: a higher-priority request fires the
96/// victim's [`preempted`](PreemptiveGuard::preempted) signal, and the victim is
97/// expected to bail via `any_of![work, guard.preempted()]`. A well-behaved
98/// holder races its work against that signal:
99///
100/// ```
101/// use simu::{SimEnv, PreemptiveResource, any_of};
102/// let mut env = SimEnv::with_seed(0);
103/// let res = PreemptiveResource::new(1);
104/// let h = env.handle();
105/// let r = res.clone();
106/// env.spawn(async move {
107/// let guard = r.request(2).await;
108/// let service = 10.0;
109/// // Race the service time against a possible preemption.
110/// any_of![h.timeout(service), guard.preempted()].await;
111/// if guard.is_preempted() {
112/// // Higher-priority work took the unit — abandon and clean up.
113/// return;
114/// }
115/// // Completed normally; dropping the guard releases the unit.
116/// });
117/// env.run();
118/// ```
119///
120/// `PreemptiveResource` wraps an `Rc<RefCell<>>` internally, so cloning is
121/// cheap and all clones share the same pool. It is `!Send + !Sync`.
122#[derive(Clone)]
123pub struct PreemptiveResource {
124 state: Rc<RefCell<PreemptiveState>>,
125}
126
127impl std::fmt::Debug for PreemptiveResource {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 let mut d = f.debug_struct("PreemptiveResource");
130 if let Ok(s) = self.state.try_borrow() {
131 d.field("in_use", &s.wq.in_use())
132 .field("capacity", &s.wq.capacity())
133 .field("queue_len", &s.wq.live_waiters());
134 }
135 d.finish_non_exhaustive()
136 }
137}
138
139impl PreemptiveResource {
140 /// Create a new preemptive resource pool with the given capacity.
141 ///
142 /// # Panics
143 ///
144 /// Panics if `capacity` is zero.
145 #[must_use]
146 pub fn new(capacity: usize) -> Self {
147 assert!(
148 capacity > 0,
149 "PreemptiveResource capacity must be at least 1"
150 );
151 PreemptiveResource {
152 state: Rc::new(RefCell::new(PreemptiveState {
153 wq: WaitQueue::new(capacity),
154 holders: Vec::new(),
155 next_holder_id: 0,
156 })),
157 }
158 }
159
160 /// Request one unit at the given `priority` (lower = higher priority).
161 ///
162 /// Resolves immediately if a unit is free **or** if a strictly
163 /// lower-priority holder can be preempted; otherwise suspends in priority
164 /// order until a unit is released or becomes preemptible.
165 ///
166 /// The returned [`PreemptiveGuard`] releases the unit when dropped, and
167 /// exposes [`preempted`](PreemptiveGuard::preempted) /
168 /// [`is_preempted`](PreemptiveGuard::is_preempted) so the holder can yield
169 /// the unit cooperatively.
170 #[must_use = "futures do nothing unless awaited"]
171 pub fn request(&self, priority: u32) -> PreemptiveRequest {
172 PreemptiveRequest {
173 state: Rc::clone(&self.state),
174 priority,
175 registered: false,
176 consumed: false,
177 canceled: Rc::new(Cell::new(false)),
178 granted: Rc::new(Cell::new(false)),
179 }
180 }
181
182 /// Number of units currently in use.
183 #[must_use]
184 pub fn in_use(&self) -> usize {
185 self.state.borrow().wq.in_use()
186 }
187
188 /// Total capacity of this resource pool.
189 #[must_use]
190 pub fn capacity(&self) -> usize {
191 self.state.borrow().wq.capacity()
192 }
193
194 /// Number of processes currently *blocked* waiting for a unit (i.e. those
195 /// that could neither take a free unit nor preempt a holder). Excludes
196 /// abandoned (canceled) requests and current holders.
197 #[must_use]
198 pub fn queue_len(&self) -> usize {
199 self.state.borrow().wq.live_waiters()
200 }
201}
202
203/// Build a guard for a freshly granted unit, registering its holder entry.
204/// Returns the guard; called from both the free-unit and preemption paths.
205fn grant(state_rc: &Rc<RefCell<PreemptiveState>>, priority: u32) -> PreemptiveGuard {
206 let (trigger, awaitable) = new_event();
207 let preempted = Rc::new(Cell::new(false));
208 let id = {
209 let mut state = state_rc.borrow_mut();
210 let id = state.next_holder_id;
211 state.next_holder_id += 1;
212 state.holders.push(Holder {
213 priority,
214 id,
215 trigger: Some(trigger),
216 preempted: Rc::clone(&preempted),
217 });
218 id
219 };
220 PreemptiveGuard {
221 state: Rc::clone(state_rc),
222 id,
223 signal: awaitable,
224 preempted,
225 }
226}
227
228/// Future returned by [`PreemptiveResource::request`].
229///
230/// Resolves to a [`PreemptiveGuard`] once a unit is acquired — either a free
231/// one, or one taken from a preempted lower-priority holder.
232pub struct PreemptiveRequest {
233 state: Rc<RefCell<PreemptiveState>>,
234 priority: u32,
235 registered: bool,
236 /// Set once a granted/acquired unit has become a holder+guard; `Drop` then
237 /// must not release (the guard owns the unit).
238 consumed: bool,
239 canceled: Rc<Cell<bool>>,
240 /// Shared with the queue entry; set to `true` by `WaitQueue::release` when a
241 /// released unit is handed directly to this blocked request. Checked first
242 /// in `poll`, exactly like the plain `Resource` handoff.
243 granted: Rc<Cell<bool>>,
244}
245
246impl std::fmt::Debug for PreemptiveRequest {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("PreemptiveRequest")
249 .field("priority", &self.priority)
250 .field("registered", &self.registered)
251 .field("granted", &self.granted.get())
252 .finish_non_exhaustive()
253 }
254}
255
256impl Future for PreemptiveRequest {
257 type Output = PreemptiveGuard;
258
259 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<PreemptiveGuard> {
260 // Direct handoff: a released unit was transferred to us by `wq.release`.
261 // `in_use` already accounts for it and the releasing guard removed its
262 // holder entry, so we simply register our own holder via grant().
263 if self.granted.get() {
264 self.consumed = true;
265 return Poll::Ready(grant(&self.state, self.priority));
266 }
267
268 // Decide the outcome while holding the borrow, but build the guard
269 // afterwards (grant() re-borrows state).
270 enum Outcome {
271 Free,
272 Preempt(usize),
273 Block,
274 }
275
276 let outcome = {
277 let mut state = self.state.borrow_mut();
278 if state.wq.try_acquire() {
279 Outcome::Free
280 } else if let Some(idx) = state.victim_index(self.priority) {
281 Outcome::Preempt(idx)
282 } else {
283 if !self.registered {
284 state.wq.register(
285 self.priority,
286 cx.waker().clone(),
287 Rc::clone(&self.canceled),
288 Rc::clone(&self.granted),
289 );
290 }
291 Outcome::Block
292 }
293 };
294
295 match outcome {
296 Outcome::Free => {
297 self.consumed = true;
298 Poll::Ready(grant(&self.state, self.priority))
299 }
300 Outcome::Preempt(idx) => {
301 // Evict the victim: fire its signal and remove its holder entry.
302 // Capacity bookkeeping is unchanged — the unit transfers
303 // directly from victim to us without passing through the queue.
304 let victim = {
305 let mut state = self.state.borrow_mut();
306 state.holders.remove(idx)
307 };
308 victim.preempted.set(true);
309 if let Some(trigger) = victim.trigger {
310 trigger.fire();
311 }
312 self.consumed = true;
313 Poll::Ready(grant(&self.state, self.priority))
314 }
315 Outcome::Block => {
316 self.registered = true;
317 Poll::Pending
318 }
319 }
320 }
321}
322
323impl Drop for PreemptiveRequest {
324 fn drop(&mut self) {
325 if self.consumed {
326 return; // the guard owns the unit and will release it
327 }
328 if self.granted.get() {
329 // A unit was handed to us but never turned into a holder (dropped
330 // before re-poll). No holder was created yet, so just release it
331 // back into the queue to hand it on to the next waiter.
332 self.state.borrow_mut().wq.release();
333 } else if self.registered {
334 self.canceled.set(true);
335 }
336 }
337}
338
339/// RAII guard holding one unit of a [`PreemptiveResource`].
340///
341/// Dropping the guard releases the unit (waking the next waiter) **unless** the
342/// unit was already preempted, in which case the drop is a no-op — the unit has
343/// already been handed to the preemptor.
344pub struct PreemptiveGuard {
345 state: Rc<RefCell<PreemptiveState>>,
346 id: u64,
347 signal: EventAwaitable,
348 preempted: Rc<Cell<bool>>,
349}
350
351impl std::fmt::Debug for PreemptiveGuard {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 f.debug_struct("PreemptiveGuard")
354 .field("id", &self.id)
355 .field("preempted", &self.preempted.get())
356 .finish_non_exhaustive()
357 }
358}
359
360impl PreemptiveGuard {
361 /// A future that resolves when this unit is preempted by a higher-priority
362 /// request. Intended for racing against the holder's work, e.g.
363 /// `any_of![env.timeout(d), guard.preempted()]`.
364 ///
365 /// Resolves immediately if preemption has already happened.
366 #[must_use = "futures do nothing unless awaited"]
367 pub fn preempted(&self) -> EventAwaitable {
368 self.signal.clone()
369 }
370
371 /// Synchronously report whether this unit has been preempted. Check this
372 /// after a race to decide whether to abandon the work.
373 #[must_use]
374 pub fn is_preempted(&self) -> bool {
375 self.preempted.get()
376 }
377}
378
379impl Drop for PreemptiveGuard {
380 fn drop(&mut self) {
381 let mut state = self.state.borrow_mut();
382 // If preempted, the holder entry is already gone and the unit was
383 // transferred to the preemptor — releasing again would double-count.
384 if self.preempted.get() {
385 return;
386 }
387 if let Some(pos) = state.holders.iter().position(|h| h.id == self.id) {
388 state.holders.remove(pos);
389 }
390 state.wq.release();
391 }
392}