Skip to main content

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}