submilli_engine/runtime/
limits.rs1use std::sync::Arc;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use wasmtime::{ResourceLimiter, Store};
10
11use super::StoreData;
12
13pub const DEFAULT_MAX_STORE_BYTES: u64 = 50 * 1024 * 1024;
14
15#[derive(Debug, Clone, Copy)]
16pub struct MemoryCapExceeded {
17 pub requested: u64,
18 pub already_observed: u64,
19 pub already_host_attached: u64,
20 pub cap: u64,
21}
22
23impl std::fmt::Display for MemoryCapExceeded {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(
26 f,
27 "memory cap exceeded: requested {} host bytes; \
28 already observed {} bytes + {} host bytes against {} cap",
29 self.requested, self.already_observed, self.already_host_attached, self.cap
30 )
31 }
32}
33
34impl std::error::Error for MemoryCapExceeded {}
35
36#[derive(Debug, Clone, Copy)]
40pub struct MemoryExhausted;
41
42impl std::fmt::Display for MemoryExhausted {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.write_str("memory exhausted")
45 }
46}
47
48pub fn is_memory_exhausted(err: &wasmtime::Error) -> bool {
51 err.is::<MemoryExhausted>()
52 || err.is::<wasmtime::GcHeapOutOfMemory<()>>()
53 || err.is::<MemoryCapExceeded>()
54}
55
56pub(crate) fn name_memory_exhaustion(err: wasmtime::Error) -> wasmtime::Error {
59 if !is_memory_exhausted(&err) || err.is::<MemoryExhausted>() {
60 return err;
61 }
62 err.context(MemoryExhausted)
63}
64
65pub(crate) struct HostBytes {
67 counter: Arc<AtomicU64>,
68 bytes: u64,
69}
70
71impl HostBytes {
72 pub(crate) fn new(limits: &TenantLimits, bytes: u64) -> Result<Self, MemoryCapExceeded> {
73 limits.charge_host_bytes(bytes)?;
74 Ok(Self {
75 counter: limits.host_attached_counter(),
76 bytes,
77 })
78 }
79}
80
81impl Drop for HostBytes {
82 fn drop(&mut self) {
83 let _ = self
84 .counter
85 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
86 Some(current.saturating_sub(self.bytes))
87 });
88 }
89}
90
91pub struct TenantLimits {
92 pub max_total_bytes: u64,
93 observed_bytes: u64,
94 peak_bytes: AtomicU64,
95 host_attached_bytes: Arc<AtomicU64>,
96}
97
98impl TenantLimits {
99 pub fn new(max_total_bytes: u64) -> Self {
100 Self {
101 max_total_bytes,
102 observed_bytes: 0,
103 peak_bytes: AtomicU64::new(0),
104 host_attached_bytes: Arc::new(AtomicU64::new(0)),
105 }
106 }
107
108 pub fn observed_bytes(&self) -> u64 {
109 self.observed_bytes
110 }
111
112 pub fn peak_bytes(&self) -> u64 {
114 self.peak_bytes.load(Ordering::Relaxed)
115 }
116
117 pub fn host_attached_bytes(&self) -> u64 {
118 self.host_attached_bytes.load(Ordering::Relaxed)
119 }
120
121 pub fn host_attached_counter(&self) -> Arc<AtomicU64> {
122 Arc::clone(&self.host_attached_bytes)
123 }
124
125 pub fn charge_host_bytes(&self, n: u64) -> Result<(), MemoryCapExceeded> {
128 self.host_attached_bytes
129 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
130 let next = current.checked_add(n)?;
131 (self.observed_bytes.saturating_add(next) <= self.max_total_bytes).then_some(next)
132 })
133 .map(|previous| {
134 self.peak_bytes.fetch_max(
135 self.observed_bytes
136 .saturating_add(previous)
137 .saturating_add(n),
138 Ordering::Relaxed,
139 );
140 })
141 .map_err(|current| MemoryCapExceeded {
142 requested: n,
143 already_observed: self.observed_bytes,
144 already_host_attached: current,
145 cap: self.max_total_bytes,
146 })
147 }
148
149 pub fn release_host_bytes(&self, n: u64) {
150 let _ = self.host_attached_bytes.fetch_update(
151 Ordering::Relaxed,
152 Ordering::Relaxed,
153 |current| Some(current.saturating_sub(n)),
154 );
155 }
156}
157
158impl ResourceLimiter for TenantLimits {
159 fn memory_growing(
160 &mut self,
161 current: usize,
162 desired: usize,
163 _maximum: Option<usize>,
164 ) -> wasmtime::Result<bool> {
165 let delta = (desired as u64).saturating_sub(current as u64);
166 let next = self.observed_bytes.saturating_add(delta);
167 let host = self.host_attached_bytes.load(Ordering::Relaxed);
169 if next.saturating_add(host) > self.max_total_bytes {
170 return Ok(false);
171 }
172 self.observed_bytes = next;
173 self.peak_bytes
174 .fetch_max(next.saturating_add(host), Ordering::Relaxed);
175 Ok(true)
176 }
177
178 fn table_growing(
179 &mut self,
180 _current: usize,
181 _desired: usize,
182 _maximum: Option<usize>,
183 ) -> wasmtime::Result<bool> {
184 Ok(true)
185 }
186}
187
188pub fn install_tenant_limits(store: &mut Store<StoreData>) {
190 store.limiter(|data| &mut data.tenant_limits);
191}
192
193#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
195pub struct ExecutionUsage {
196 pub fuel: u64,
198 pub wasm_fuel: u64,
200 pub host_fuel: u64,
203 pub memory_peak: u64,
204}
205
206impl ExecutionUsage {
207 pub fn capture(store: &Store<StoreData>, initial_fuel: u64) -> wasmtime::Result<Self> {
208 let remaining = store.get_fuel()?;
209 let fuel = initial_fuel
210 .checked_sub(remaining)
211 .ok_or_else(|| wasmtime::Error::msg("remaining fuel exceeds initial budget"))?;
212 let fuel = fuel
216 .saturating_add(store.data().host_fuel_pending)
217 .min(initial_fuel);
218 let host_fuel = store.data().host_fuel;
219 let wasm_fuel = fuel
220 .checked_sub(host_fuel)
221 .ok_or_else(|| wasmtime::Error::msg("host fuel exceeds the fuel spent"))?;
222 Ok(Self {
223 fuel,
224 wasm_fuel,
225 host_fuel,
226 memory_peak: store.data().tenant_limits.peak_bytes(),
227 })
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn memory_peak_retains_released_host_allocations_and_excludes_refusals() {
237 let mut limits = TenantLimits::new(1000);
238 assert!(limits.memory_growing(0, 200, None).unwrap());
239 limits.charge_host_bytes(500).unwrap();
240 limits.release_host_bytes(500);
241 assert_eq!(limits.peak_bytes(), 700);
242 limits.charge_host_bytes(100).unwrap();
243 assert!(limits.memory_growing(200, 650, None).unwrap());
244 assert_eq!(limits.peak_bytes(), 750);
245 assert!(limits.charge_host_bytes(300).is_err());
246 assert!(!limits.memory_growing(650, 950, None).unwrap());
247 assert_eq!(limits.peak_bytes(), 750);
248 }
249}