simu/resource/container.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//! Continuous-quantity reservoir.
6//!
7//! [`Container`] models tanks, silos, batteries, stockpiles — anything
8//! measured in amounts rather than discrete units. `put(amount)` /
9//! `get(amount)` suspend when they cannot complete, with strict head-of-line
10//! FIFO waiters on both sides.
11
12use std::cell::{Cell, RefCell};
13use std::collections::VecDeque;
14use std::future::Future;
15use std::pin::Pin;
16use std::rc::Rc;
17use std::task::{Context, Poll, Waker};
18
19struct GetWaiter {
20 amount: f64,
21 waker: Waker,
22 done: Rc<Cell<bool>>,
23 /// Shared with the owning `ContainerGetRequest`. Set to `true` if the
24 /// future is dropped before being granted; the cascade skips canceled
25 /// entries so the level is not deducted for an abandoned request.
26 canceled: Rc<Cell<bool>>,
27}
28
29struct PutWaiter {
30 amount: f64,
31 waker: Waker,
32 done: Rc<Cell<bool>>,
33 canceled: Rc<Cell<bool>>,
34}
35
36struct ContainerState {
37 capacity: f64,
38 level: f64,
39 get_waiters: VecDeque<GetWaiter>,
40 put_waiters: VecDeque<PutWaiter>,
41}
42
43// ---------------------------------------------------------------------------
44// FIFO guards
45// ---------------------------------------------------------------------------
46
47/// True if at least one non-canceled `get` waiter is already queued.
48///
49/// A freshly-arriving `get` must not take level ahead of such a waiter, even
50/// when the current level would cover it — that would violate the documented
51/// head-of-line FIFO contract (and diverge from SimPy). Canceled entries (from
52/// abandoned requests) don't count: they are skipped by the cascade and hold no
53/// claim on the level.
54fn has_live_get_waiter(state: &ContainerState) -> bool {
55 state.get_waiters.iter().any(|w| !w.canceled.get())
56}
57
58/// True if at least one non-canceled `put` waiter is already queued. Symmetric
59/// to [`has_live_get_waiter`]: a fresh `put` must not take space ahead of an
60/// earlier blocked put.
61fn has_live_put_waiter(state: &ContainerState) -> bool {
62 state.put_waiters.iter().any(|w| !w.canceled.get())
63}
64
65// ---------------------------------------------------------------------------
66// Wake cascade helpers
67// ---------------------------------------------------------------------------
68
69/// Drain as many head-of-queue get waiters as current level allows (FIFO).
70/// Canceled entries (from abandoned requests) are skipped without touching level.
71///
72/// Returns `true` if at least one live waiter was serviced — i.e. the level
73/// changed and the *other* queue may now have become unblocked.
74fn wake_get_waiters(state: &mut ContainerState) -> bool {
75 let mut serviced = false;
76 while let Some(front) = state.get_waiters.front() {
77 if front.canceled.get() {
78 state.get_waiters.pop_front();
79 continue;
80 }
81 if state.level >= front.amount {
82 let w = state.get_waiters.pop_front().unwrap();
83 state.level -= w.amount;
84 w.done.set(true);
85 w.waker.wake();
86 serviced = true;
87 } else {
88 break; // FIFO: head is blocked, nobody behind it can proceed
89 }
90 }
91 serviced
92}
93
94/// Drain as many head-of-queue put waiters as available space allows (FIFO).
95/// Canceled entries are skipped without touching level.
96///
97/// Returns `true` if at least one live waiter was serviced.
98fn wake_put_waiters(state: &mut ContainerState) -> bool {
99 let mut serviced = false;
100 while let Some(front) = state.put_waiters.front() {
101 if front.canceled.get() {
102 state.put_waiters.pop_front();
103 continue;
104 }
105 if state.level + front.amount <= state.capacity {
106 let w = state.put_waiters.pop_front().unwrap();
107 state.level += w.amount;
108 w.done.set(true);
109 w.waker.wake();
110 serviced = true;
111 } else {
112 break;
113 }
114 }
115 serviced
116}
117
118/// Run get/put cascades until no more progress is possible.
119///
120/// Loops while either queue services a waiter, since satisfying a get frees
121/// space (possibly unblocking a put) and satisfying a put adds material
122/// (possibly unblocking a get). Termination is driven by whether any waiter
123/// was actually serviced — never by a float-level comparison — so a pass whose
124/// gets and puts net to a zero level change still triggers another iteration
125/// when it leaves a newly-serviceable waiter behind. Each serviced waiter
126/// removes an entry from a finite queue, so the loop always terminates.
127fn trigger_cascade(state: &mut ContainerState) {
128 loop {
129 let serviced_get = wake_get_waiters(state);
130 let serviced_put = wake_put_waiters(state);
131 if !serviced_get && !serviced_put {
132 break;
133 }
134 }
135}
136
137// ---------------------------------------------------------------------------
138// Public types
139// ---------------------------------------------------------------------------
140
141/// A cloneable handle to a continuous-quantity resource (e.g., a tank of
142/// liquid, a battery, an inventory of medication).
143///
144/// `put(amount)` adds material; `get(amount)` removes it. Both operations
145/// suspend the calling process when they cannot immediately complete:
146///
147/// - `get` suspends when the current level is below the requested amount.
148/// - `put` suspends when adding the amount would exceed the container's capacity.
149///
150/// Waiters are served in **strict head-of-line FIFO** within each queue: a
151/// freshly-arriving request never takes level/space ahead of an already-queued
152/// waiter, even when the current level would let it complete immediately. A
153/// blocked head-of-queue request therefore holds the line for everyone behind
154/// it (matching SimPy's `Container`). All clones share the same internal state
155/// (cheap `Rc` clone). `Container` is `!Send + !Sync`, consistent with `SimEnv`.
156///
157/// A fuel tank: the car needs more than is in stock, so it waits for the
158/// tanker truck's delivery:
159///
160/// ```
161/// use simu::{SimEnv, Container};
162///
163/// let mut env = SimEnv::with_seed(0);
164/// let tank = Container::new(100.0, 20.0); // capacity 100, starts at 20
165///
166/// // A truck delivers 80 units at t = 5.
167/// let h = env.handle();
168/// let t = tank.clone();
169/// env.spawn(async move {
170/// h.timeout(5.0).await;
171/// t.put(80.0).await; // fits (20 + 80 ≤ 100), resolves immediately
172/// });
173///
174/// // A car wants 50 units — more than the current level, so it suspends.
175/// let h2 = env.handle();
176/// let t2 = tank.clone();
177/// env.spawn(async move {
178/// t2.get(50.0).await; // woken by the delivery
179/// assert_eq!(h2.now(), 5.0);
180/// });
181///
182/// env.run();
183/// assert_eq!(tank.level(), 50.0); // 20 + 80 − 50
184/// ```
185#[derive(Clone)]
186pub struct Container {
187 state: Rc<RefCell<ContainerState>>,
188}
189
190impl std::fmt::Debug for Container {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 let mut d = f.debug_struct("Container");
193 if let Ok(s) = self.state.try_borrow() {
194 d.field("level", &s.level)
195 .field("capacity", &s.capacity)
196 .field("get_waiters", &s.get_waiters.len())
197 .field("put_waiters", &s.put_waiters.len());
198 }
199 d.finish_non_exhaustive()
200 }
201}
202
203impl Container {
204 /// Create an **empty** container with the given capacity.
205 ///
206 /// # Panics
207 /// Panics if `capacity <= 0`.
208 #[must_use]
209 pub fn empty(capacity: f64) -> Self {
210 Self::new(capacity, 0.0)
211 }
212
213 /// Create a container with the given capacity and initial level.
214 ///
215 /// # Panics
216 /// Panics if `capacity <= 0`, `initial_level < 0`, or
217 /// `initial_level > capacity`.
218 #[must_use]
219 pub fn new(capacity: f64, initial_level: f64) -> Self {
220 assert!(capacity > 0.0, "Container capacity must be positive");
221 assert!(
222 initial_level >= 0.0,
223 "Container initial_level must be non-negative"
224 );
225 assert!(
226 initial_level <= capacity,
227 "Container initial_level must not exceed capacity"
228 );
229 Container {
230 state: Rc::new(RefCell::new(ContainerState {
231 capacity,
232 level: initial_level,
233 get_waiters: VecDeque::new(),
234 put_waiters: VecDeque::new(),
235 })),
236 }
237 }
238
239 /// Current level (amount of material present).
240 #[must_use]
241 pub fn level(&self) -> f64 {
242 self.state.borrow().level
243 }
244
245 /// Maximum capacity.
246 #[must_use]
247 pub fn capacity(&self) -> f64 {
248 self.state.borrow().capacity
249 }
250
251 /// Number of consumers currently blocked in the `get` queue (waiting for
252 /// enough material). Excludes abandoned (canceled) requests.
253 #[must_use]
254 pub fn get_queue_len(&self) -> usize {
255 self.state
256 .borrow()
257 .get_waiters
258 .iter()
259 .filter(|w| !w.canceled.get())
260 .count()
261 }
262
263 /// Number of producers currently blocked in the `put` queue (waiting for
264 /// enough free space). Excludes abandoned (canceled) requests.
265 #[must_use]
266 pub fn put_queue_len(&self) -> usize {
267 self.state
268 .borrow()
269 .put_waiters
270 .iter()
271 .filter(|w| !w.canceled.get())
272 .count()
273 }
274
275 /// Add `amount` to the container.
276 ///
277 /// Resolves immediately if `level + amount <= capacity`; otherwise
278 /// suspends until enough space is available.
279 ///
280 /// # Panics
281 /// Panics if `amount <= 0`, or if `amount > capacity` — the latter could
282 /// never complete and, under strict head-of-line FIFO, would block every
283 /// later waiter behind it, so it is treated as a programming error. (SimPy
284 /// blocks forever here instead; diverging is deliberate.)
285 #[must_use = "futures do nothing unless awaited"]
286 pub fn put(&self, amount: f64) -> ContainerPutRequest {
287 assert!(amount > 0.0, "Container::put amount must be positive");
288 let capacity = self.state.borrow().capacity;
289 assert!(
290 amount <= capacity,
291 "Container::put amount ({amount}) exceeds capacity ({capacity}); it could never complete"
292 );
293 ContainerPutRequest {
294 state: Rc::clone(&self.state),
295 amount,
296 registered: false,
297 done: Rc::new(Cell::new(false)),
298 canceled: Rc::new(Cell::new(false)),
299 }
300 }
301
302 /// Remove `amount` from the container.
303 ///
304 /// Resolves immediately if `level >= amount`; otherwise suspends until
305 /// enough material is available.
306 ///
307 /// # Panics
308 /// Panics if `amount <= 0`, or if `amount > capacity` — the latter could
309 /// never complete and, under strict head-of-line FIFO, would block every
310 /// later waiter behind it, so it is treated as a programming error. (SimPy
311 /// blocks forever here instead; diverging is deliberate.)
312 #[must_use = "futures do nothing unless awaited"]
313 pub fn get(&self, amount: f64) -> ContainerGetRequest {
314 assert!(amount > 0.0, "Container::get amount must be positive");
315 let capacity = self.state.borrow().capacity;
316 assert!(
317 amount <= capacity,
318 "Container::get amount ({amount}) exceeds capacity ({capacity}); it could never complete"
319 );
320 ContainerGetRequest {
321 state: Rc::clone(&self.state),
322 amount,
323 registered: false,
324 done: Rc::new(Cell::new(false)),
325 canceled: Rc::new(Cell::new(false)),
326 }
327 }
328}
329
330// ---------------------------------------------------------------------------
331// ContainerPutRequest
332// ---------------------------------------------------------------------------
333
334/// Future returned by [`Container::put`].
335pub struct ContainerPutRequest {
336 state: Rc<RefCell<ContainerState>>,
337 amount: f64,
338 registered: bool,
339 /// Shared with the `PutWaiter` entry; the cascade sets this to `true`
340 /// before calling `waker.wake()`, so the next poll can return `Ready`
341 /// without re-checking the level.
342 done: Rc<Cell<bool>>,
343 /// Shared with the `PutWaiter` entry; the request's `Drop` impl sets this
344 /// to `true` if the future is abandoned before being granted, so the
345 /// cascade skips the entry without adding level.
346 canceled: Rc<Cell<bool>>,
347}
348
349impl std::fmt::Debug for ContainerPutRequest {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 f.debug_struct("ContainerPutRequest")
352 .field("amount", &self.amount)
353 .field("registered", &self.registered)
354 .field("done", &self.done.get())
355 .finish_non_exhaustive()
356 }
357}
358
359impl Future for ContainerPutRequest {
360 type Output = ();
361
362 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
363 // Cascade already committed our put — no need to touch level again.
364 if self.done.get() {
365 return Poll::Ready(());
366 }
367 {
368 let mut state = self.state.borrow_mut();
369 // Fast path only when nothing is queued ahead of us: taking space
370 // out-of-turn would let a fresh put jump an earlier blocked put,
371 // violating FIFO.
372 if !self.registered
373 && state.level + self.amount <= state.capacity
374 && !has_live_put_waiter(&state)
375 {
376 state.level += self.amount;
377 // Full cascade, not just wake_get_waiters: a woken get may drain
378 // the level and free space for a blocked put-waiter behind it.
379 // Using only wake_get_waiters here would strand that put-waiter.
380 trigger_cascade(&mut state);
381 return Poll::Ready(());
382 }
383 if !self.registered {
384 state.put_waiters.push_back(PutWaiter {
385 amount: self.amount,
386 waker: cx.waker().clone(),
387 done: Rc::clone(&self.done),
388 canceled: Rc::clone(&self.canceled),
389 });
390 }
391 }
392 self.registered = true;
393 Poll::Pending
394 }
395}
396
397impl Drop for ContainerPutRequest {
398 fn drop(&mut self) {
399 // If we registered but never completed (cascade would have set
400 // `done`), mark the queue entry canceled so the cascade skips it.
401 if self.registered && !self.done.get() {
402 self.canceled.set(true);
403 }
404 }
405}
406
407// ---------------------------------------------------------------------------
408// ContainerGetRequest
409// ---------------------------------------------------------------------------
410
411/// Future returned by [`Container::get`].
412pub struct ContainerGetRequest {
413 state: Rc<RefCell<ContainerState>>,
414 amount: f64,
415 registered: bool,
416 /// Shared with the `GetWaiter` entry; the cascade sets this to `true`
417 /// before calling `waker.wake()`, so the next poll can return `Ready`.
418 done: Rc<Cell<bool>>,
419 /// Shared with the `GetWaiter` entry; the request's `Drop` impl sets this
420 /// to `true` if the future is abandoned before being granted, so the
421 /// cascade skips the entry without deducting level.
422 canceled: Rc<Cell<bool>>,
423}
424
425impl std::fmt::Debug for ContainerGetRequest {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 f.debug_struct("ContainerGetRequest")
428 .field("amount", &self.amount)
429 .field("registered", &self.registered)
430 .field("done", &self.done.get())
431 .finish_non_exhaustive()
432 }
433}
434
435impl Future for ContainerGetRequest {
436 type Output = ();
437
438 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
439 // Cascade already committed our get.
440 if self.done.get() {
441 return Poll::Ready(());
442 }
443 {
444 let mut state = self.state.borrow_mut();
445 // Only take level immediately if we haven't yet registered as a
446 // waiter *and* no live waiter is queued ahead of us — taking level
447 // out-of-turn would let a fresh (e.g. smaller) get jump an earlier
448 // blocked get, violating FIFO.
449 if !self.registered && state.level >= self.amount && !has_live_get_waiter(&state) {
450 state.level -= self.amount;
451 trigger_cascade(&mut state);
452 return Poll::Ready(());
453 }
454 if !self.registered {
455 state.get_waiters.push_back(GetWaiter {
456 amount: self.amount,
457 waker: cx.waker().clone(),
458 done: Rc::clone(&self.done),
459 canceled: Rc::clone(&self.canceled),
460 });
461 }
462 }
463 self.registered = true;
464 Poll::Pending
465 }
466}
467
468impl Drop for ContainerGetRequest {
469 fn drop(&mut self) {
470 if self.registered && !self.done.get() {
471 self.canceled.set(true);
472 }
473 }
474}