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 struct HostBytes {
67 counter: Arc<AtomicU64>,
68 bytes: u64,
69}
70
71impl HostBytes {
72 pub 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 pub(crate) fn grow(
80 &mut self,
81 limits: &TenantLimits,
82 bytes: u64,
83 ) -> Result<(), MemoryCapExceeded> {
84 limits.charge_host_bytes(bytes)?;
85 self.bytes = self.bytes.saturating_add(bytes);
86 Ok(())
87 }
88}
89
90impl Drop for HostBytes {
91 fn drop(&mut self) {
92 let _ = self
93 .counter
94 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
95 Some(current.saturating_sub(self.bytes))
96 });
97 }
98}
99
100pub struct TenantLimits {
101 pub max_total_bytes: u64,
102 observed_bytes: u64,
103 peak_bytes: AtomicU64,
104 host_attached_bytes: Arc<AtomicU64>,
105}
106
107impl TenantLimits {
108 pub fn new(max_total_bytes: u64) -> Self {
109 Self {
110 max_total_bytes,
111 observed_bytes: 0,
112 peak_bytes: AtomicU64::new(0),
113 host_attached_bytes: Arc::new(AtomicU64::new(0)),
114 }
115 }
116
117 pub fn observed_bytes(&self) -> u64 {
118 self.observed_bytes
119 }
120
121 pub fn peak_bytes(&self) -> u64 {
123 self.peak_bytes.load(Ordering::Relaxed)
124 }
125
126 pub fn host_attached_bytes(&self) -> u64 {
127 self.host_attached_bytes.load(Ordering::Relaxed)
128 }
129
130 pub fn host_attached_counter(&self) -> Arc<AtomicU64> {
131 Arc::clone(&self.host_attached_bytes)
132 }
133
134 pub fn charge_host_bytes(&self, n: u64) -> Result<(), MemoryCapExceeded> {
137 self.host_attached_bytes
138 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
139 let next = current.checked_add(n)?;
140 (self.observed_bytes.saturating_add(next) <= self.max_total_bytes).then_some(next)
141 })
142 .map(|previous| {
143 self.peak_bytes.fetch_max(
144 self.observed_bytes
145 .saturating_add(previous)
146 .saturating_add(n),
147 Ordering::Relaxed,
148 );
149 })
150 .map_err(|current| MemoryCapExceeded {
151 requested: n,
152 already_observed: self.observed_bytes,
153 already_host_attached: current,
154 cap: self.max_total_bytes,
155 })
156 }
157
158 pub fn release_host_bytes(&self, n: u64) {
159 let _ = self.host_attached_bytes.fetch_update(
160 Ordering::Relaxed,
161 Ordering::Relaxed,
162 |current| Some(current.saturating_sub(n)),
163 );
164 }
165}
166
167impl ResourceLimiter for TenantLimits {
168 fn memory_growing(
169 &mut self,
170 current: usize,
171 desired: usize,
172 _maximum: Option<usize>,
173 ) -> wasmtime::Result<bool> {
174 let delta = (desired as u64).saturating_sub(current as u64);
175 let next = self.observed_bytes.saturating_add(delta);
176 let host = self.host_attached_bytes.load(Ordering::Relaxed);
178 if next.saturating_add(host) > self.max_total_bytes {
179 return Ok(false);
180 }
181 self.observed_bytes = next;
182 self.peak_bytes
183 .fetch_max(next.saturating_add(host), Ordering::Relaxed);
184 Ok(true)
185 }
186
187 fn table_growing(
188 &mut self,
189 _current: usize,
190 _desired: usize,
191 _maximum: Option<usize>,
192 ) -> wasmtime::Result<bool> {
193 Ok(true)
194 }
195}
196
197pub fn install_tenant_limits(store: &mut Store<StoreData>) {
199 store.limiter(|data| &mut data.tenant_limits);
200}
201
202#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
204pub struct ExecutionUsage {
205 pub fuel: u64,
207 pub wasm_fuel: u64,
209 pub host_fuel: u64,
212 pub memory_peak: u64,
213}
214
215impl ExecutionUsage {
216 pub fn capture(store: &Store<StoreData>, initial_fuel: u64) -> wasmtime::Result<Self> {
217 let remaining = store.get_fuel()?;
218 let fuel = initial_fuel
219 .checked_sub(remaining)
220 .ok_or_else(|| wasmtime::Error::msg("remaining fuel exceeds initial budget"))?;
221 let fuel = fuel
225 .saturating_add(store.data().host_fuel_pending)
226 .min(initial_fuel);
227 let host_fuel = store.data().host_fuel;
228 let wasm_fuel = fuel
229 .checked_sub(host_fuel)
230 .ok_or_else(|| wasmtime::Error::msg("host fuel exceeds the fuel spent"))?;
231 Ok(Self {
232 fuel,
233 wasm_fuel,
234 host_fuel,
235 memory_peak: store.data().tenant_limits.peak_bytes(),
236 })
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn memory_peak_retains_released_host_allocations_and_excludes_refusals() {
246 let mut limits = TenantLimits::new(1000);
247 assert!(limits.memory_growing(0, 200, None).unwrap());
248 limits.charge_host_bytes(500).unwrap();
249 limits.release_host_bytes(500);
250 assert_eq!(limits.peak_bytes(), 700);
251 limits.charge_host_bytes(100).unwrap();
252 assert!(limits.memory_growing(200, 650, None).unwrap());
253 assert_eq!(limits.peak_bytes(), 750);
254 assert!(limits.charge_host_bytes(300).is_err());
255 assert!(!limits.memory_growing(650, 950, None).unwrap());
256 assert_eq!(limits.peak_bytes(), 750);
257 }
258}