Skip to main content

monoloop_loop/transaction/lifecycle/
capacity.rs

1//! RAII transaction reservations (v2 §9.3).
2//!
3//! Counter-only acquire/release APIs are forbidden. Zero capacities are rejected
4//! at construction — never silently substituted with `.max(1)`.
5
6use monoloop_contracts::ChannelId;
7use std::collections::HashMap;
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::Arc;
10
11/// Runtime-wide reservation pool installed at start.
12#[derive(Debug)]
13pub struct ReservationPool {
14    max_global: usize,
15    max_ledger: usize,
16    global_active: AtomicUsize,
17    ledger_active: AtomicUsize,
18    per_channel: HashMap<ChannelId, ChannelSlot>,
19}
20
21#[derive(Debug)]
22struct ChannelSlot {
23    max: usize,
24    active: AtomicUsize,
25}
26
27/// Invalid reservation-pool construction.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum ReservationPoolError {
30    /// Global capacity was zero.
31    ZeroGlobal,
32    /// A per-channel capacity was zero.
33    ZeroChannel,
34}
35
36/// RAII permits held for one admitted transaction until cleanup releases them.
37#[derive(Debug)]
38pub struct TransactionReservations {
39    pool: Arc<ReservationPool>,
40    channel_id: ChannelId,
41    global: Option<GlobalPermit>,
42    channel: Option<ChannelPermit>,
43    ledger: Option<LedgerPermit>,
44}
45
46#[derive(Debug)]
47struct GlobalPermit {
48    pool: Arc<ReservationPool>,
49}
50
51#[derive(Debug)]
52struct ChannelPermit {
53    pool: Arc<ReservationPool>,
54    channel_id: ChannelId,
55}
56
57#[derive(Debug)]
58struct LedgerPermit {
59    pool: Arc<ReservationPool>,
60}
61
62impl Drop for GlobalPermit {
63    fn drop(&mut self) {
64        self.pool.global_active.fetch_sub(1, Ordering::SeqCst);
65    }
66}
67
68impl Drop for ChannelPermit {
69    fn drop(&mut self) {
70        if let Some(ch) = self.pool.per_channel.get(&self.channel_id) {
71            ch.active.fetch_sub(1, Ordering::SeqCst);
72        }
73    }
74}
75
76impl Drop for LedgerPermit {
77    fn drop(&mut self) {
78        self.pool.ledger_active.fetch_sub(1, Ordering::SeqCst);
79    }
80}
81
82impl ReservationPool {
83    /// Build from global and per-channel maxima. Ledger capacity equals global.
84    ///
85    /// Returns an error when any capacity is zero (fail closed; no silent bump).
86    pub fn try_new(
87        max_global: usize,
88        channels: impl IntoIterator<Item = (ChannelId, usize)>,
89    ) -> Result<Arc<Self>, ReservationPoolError> {
90        if max_global == 0 {
91            return Err(ReservationPoolError::ZeroGlobal);
92        }
93        let mut per_channel = HashMap::new();
94        for (id, max) in channels {
95            if max == 0 {
96                return Err(ReservationPoolError::ZeroChannel);
97            }
98            per_channel.insert(
99                id,
100                ChannelSlot {
101                    max,
102                    active: AtomicUsize::new(0),
103                },
104            );
105        }
106        Ok(Arc::new(Self {
107            max_global,
108            max_ledger: max_global,
109            global_active: AtomicUsize::new(0),
110            ledger_active: AtomicUsize::new(0),
111            per_channel,
112        }))
113    }
114
115    /// Observability: global active reservations.
116    pub fn global_active(&self) -> usize {
117        self.global_active.load(Ordering::SeqCst)
118    }
119
120    /// Observability: ledger entries reserved.
121    pub fn ledger_active(&self) -> usize {
122        self.ledger_active.load(Ordering::SeqCst)
123    }
124
125    /// Observability: active reservations for one Channel.
126    pub fn channel_active(&self, id: &ChannelId) -> usize {
127        self.per_channel
128            .get(id)
129            .map(|ch| ch.active.load(Ordering::SeqCst))
130            .unwrap_or(0)
131    }
132
133    /// Configured global maximum.
134    pub fn max_global(&self) -> usize {
135        self.max_global
136    }
137
138    /// Try to acquire the full reservation bundle without waiting.
139    pub fn try_reserve(self: &Arc<Self>, channel: &ChannelId) -> Option<TransactionReservations> {
140        let ch = self.per_channel.get(channel)?;
141
142        // Global
143        loop {
144            let g = self.global_active.load(Ordering::SeqCst);
145            if g >= self.max_global {
146                return None;
147            }
148            if self
149                .global_active
150                .compare_exchange(g, g + 1, Ordering::SeqCst, Ordering::SeqCst)
151                .is_ok()
152            {
153                break;
154            }
155        }
156        let global = GlobalPermit {
157            pool: Arc::clone(self),
158        };
159
160        // Channel
161        loop {
162            let c = ch.active.load(Ordering::SeqCst);
163            if c >= ch.max {
164                drop(global);
165                return None;
166            }
167            if ch
168                .active
169                .compare_exchange(c, c + 1, Ordering::SeqCst, Ordering::SeqCst)
170                .is_ok()
171            {
172                break;
173            }
174        }
175        let channel_permit = ChannelPermit {
176            pool: Arc::clone(self),
177            channel_id: channel.clone(),
178        };
179
180        // Ledger slot
181        loop {
182            let l = self.ledger_active.load(Ordering::SeqCst);
183            if l >= self.max_ledger {
184                drop(channel_permit);
185                drop(global);
186                return None;
187            }
188            if self
189                .ledger_active
190                .compare_exchange(l, l + 1, Ordering::SeqCst, Ordering::SeqCst)
191                .is_ok()
192            {
193                break;
194            }
195        }
196        let ledger = LedgerPermit {
197            pool: Arc::clone(self),
198        };
199
200        Some(TransactionReservations {
201            pool: Arc::clone(self),
202            channel_id: channel.clone(),
203            global: Some(global),
204            channel: Some(channel_permit),
205            ledger: Some(ledger),
206        })
207    }
208}
209
210impl TransactionReservations {
211    /// Channel this reservation was taken for.
212    pub fn channel_id(&self) -> &ChannelId {
213        &self.channel_id
214    }
215
216    /// Explicitly release (also runs on Drop).
217    pub fn release(self) {
218        drop(self);
219    }
220}
221
222impl Drop for TransactionReservations {
223    fn drop(&mut self) {
224        self.ledger.take();
225        self.channel.take();
226        self.global.take();
227        let _ = &self.pool;
228    }
229}