submilli_engine/stdlib/fs/
handles.rs1use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12
13use crate::runtime::durable::EffectId;
14use crate::runtime::filesystem::FilesystemError;
15use crate::runtime::fuel::WorkBudget;
16use crate::runtime::limits::{MemoryCapExceeded, TenantLimits};
17
18pub use crate::runtime::default_filesystem::kind_of;
19
20const HANDLE_BUF_BYTES: u64 = 8 * 1024;
23
24pub(super) struct ByteCharge {
26 bytes: u64,
27 counter: Arc<AtomicU64>,
28}
29
30impl ByteCharge {
31 pub(super) fn new(limits: &TenantLimits, bytes: u64) -> Result<Self, MemoryCapExceeded> {
32 limits.charge_host_bytes(bytes)?;
33 Ok(Self {
34 bytes,
35 counter: limits.host_attached_counter(),
36 })
37 }
38
39 pub(super) fn grow(
40 &mut self,
41 limits: &TenantLimits,
42 bytes: u64,
43 ) -> Result<(), MemoryCapExceeded> {
44 limits.charge_host_bytes(bytes)?;
45 self.bytes = self.bytes.saturating_add(bytes);
46 Ok(())
47 }
48
49 fn release(&mut self) {
51 let n = std::mem::take(&mut self.bytes);
52 if n == 0 {
53 return;
54 }
55 let _ = self
56 .counter
57 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
58 Some(current.saturating_sub(n))
59 });
60 }
61}
62
63impl Drop for ByteCharge {
64 fn drop(&mut self) {
65 self.release();
66 }
67}
68
69#[async_trait::async_trait]
71pub trait Closable: Send + Sync {
72 async fn close(
73 &mut self,
74 effect_id: &EffectId,
75 work: Option<Arc<WorkBudget>>,
76 limits: &TenantLimits,
77 ) -> Result<(), FilesystemError>;
78}
79
80pub struct BackendReader {
81 pub reader: Box<dyn crate::runtime::filesystem::FileReader>,
82 charge: ByteCharge,
83}
84impl BackendReader {
85 pub fn new(
86 reader: Box<dyn crate::runtime::filesystem::FileReader>,
87 limits: &TenantLimits,
88 ) -> Result<Self, MemoryCapExceeded> {
89 Ok(Self {
90 reader,
91 charge: ByteCharge::new(limits, HANDLE_BUF_BYTES)?,
92 })
93 }
94 pub fn descriptor(&self) -> Option<crate::runtime::filesystem::ResourceDescriptor> {
95 self.reader.descriptor()
96 }
97}
98#[async_trait::async_trait]
99impl Closable for BackendReader {
100 async fn close(
101 &mut self,
102 effect_id: &EffectId,
103 work: Option<Arc<WorkBudget>>,
104 _limits: &TenantLimits,
105 ) -> Result<(), FilesystemError> {
106 self.reader.close(effect_id, work).await?;
107 self.charge.release();
108 Ok(())
109 }
110}
111
112pub struct ChargedDirIter {
113 pub walk: Box<dyn crate::runtime::filesystem::DirectoryReader>,
114 charge: ByteCharge,
115}
116impl ChargedDirIter {
117 pub fn new(
118 walk: Box<dyn crate::runtime::filesystem::DirectoryReader>,
119 limits: &TenantLimits,
120 ) -> Result<Self, MemoryCapExceeded> {
121 Ok(Self {
122 charge: ByteCharge::new(limits, HANDLE_BUF_BYTES.max(walk.retained_bytes()))?,
123 walk,
124 })
125 }
126 pub fn descriptor(&self) -> Option<crate::runtime::filesystem::ResourceDescriptor> {
127 self.walk.descriptor()
128 }
129}
130#[async_trait::async_trait]
131impl Closable for ChargedDirIter {
132 async fn close(
133 &mut self,
134 effect_id: &EffectId,
135 work: Option<Arc<WorkBudget>>,
136 _limits: &TenantLimits,
137 ) -> Result<(), FilesystemError> {
138 self.walk.close(effect_id, work, _limits).await?;
139 self.charge.release();
140 Ok(())
141 }
142}
143
144pub struct BackendFileWriter {
146 writer: Box<dyn crate::runtime::filesystem::FileWriter>,
147 charge: ByteCharge,
148}
149impl BackendFileWriter {
150 pub fn descriptor(&self) -> Option<crate::runtime::filesystem::ResourceDescriptor> {
151 self.writer.descriptor()
152 }
153 pub fn new(
154 writer: Box<dyn crate::runtime::filesystem::FileWriter>,
155 limits: &TenantLimits,
156 ) -> Result<Self, MemoryCapExceeded> {
157 Ok(Self {
158 writer,
159 charge: ByteCharge::new(limits, HANDLE_BUF_BYTES)?,
160 })
161 }
162 pub async fn write(
163 &mut self,
164 effect_id: &EffectId,
165 work: Option<Arc<WorkBudget>>,
166 bytes: Vec<u8>,
167 ) -> Result<(), FilesystemError> {
168 self.writer.write(effect_id, work, bytes).await
169 }
170 pub async fn close(
171 &mut self,
172 effect_id: &EffectId,
173 work: Option<Arc<WorkBudget>>,
174 ) -> Result<(), FilesystemError> {
175 let result = self.writer.close(effect_id, work).await;
176 if self.writer.is_closed() {
177 self.charge.release();
178 }
179 result
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[tokio::test]
188 async fn failed_native_backend_close_refunds_a_discarded_buffer() {
189 use crate::runtime::default_filesystem::DefaultFilesystem;
190 use crate::runtime::filesystem::Filesystem;
191 let root = tempfile::tempdir().unwrap();
192 let filesystem = DefaultFilesystem::new(
193 crate::runtime::Vfs::external(root.path().to_path_buf()).unwrap(),
194 );
195 let effect_id = EffectId::stub().unwrap();
196 let limits = TenantLimits::new(64 * 1024);
197 let writer = filesystem
198 .open_writer(&effect_id, None, "/target")
199 .await
200 .unwrap();
201 let mut handle = BackendFileWriter::new(writer, &limits).unwrap();
202 handle
203 .write(&effect_id, None, b"data".to_vec())
204 .await
205 .unwrap();
206 std::fs::create_dir(root.path().join("target")).unwrap();
207 assert!(handle.close(&effect_id, None).await.is_err());
208 assert_eq!(limits.host_attached_bytes(), 0);
209 assert!(handle.close(&effect_id, None).await.is_ok());
210 }
211
212 async fn writer_over(vfs: &crate::runtime::Vfs, limits: &TenantLimits) -> BackendFileWriter {
213 use crate::runtime::default_filesystem::DefaultFilesystem;
214 use crate::runtime::filesystem::Filesystem;
215 let writer = DefaultFilesystem::new(vfs.clone())
216 .open_writer(&EffectId::stub().unwrap(), None, "/out.txt")
217 .await
218 .unwrap();
219 BackendFileWriter::new(writer, limits).unwrap()
220 }
221
222 fn temp_leftovers(root: &std::path::Path) -> Vec<std::ffi::OsString> {
223 std::fs::read_dir(root)
224 .unwrap()
225 .map(|e| e.unwrap().file_name())
226 .filter(|n| n.to_string_lossy().starts_with("out.txt."))
227 .collect()
228 }
229
230 #[tokio::test]
231 async fn unclosed_writer_drop_removes_temp_and_skips_rename() {
232 let limits = TenantLimits::new(10 * 1024 * 1024);
233 let dir = tempfile::tempdir().unwrap();
234 let vfs = crate::runtime::Vfs::external(dir.path().to_path_buf()).unwrap();
235 let mut w = writer_over(&vfs, &limits).await;
236 w.write(&EffectId::stub().unwrap(), None, b"hello\n".to_vec())
237 .await
238 .unwrap();
239 assert!(limits.host_attached_bytes() > 0);
240 drop(w);
241 assert!(
242 temp_leftovers(dir.path()).is_empty(),
243 "temp file removed on drop"
244 );
245 assert!(
246 !dir.path().join("out.txt").exists(),
247 "no rename without explicit close"
248 );
249 assert_eq!(limits.host_attached_bytes(), 0);
250 }
251
252 #[tokio::test]
253 async fn writer_close_renames_and_refunds() {
254 let limits = TenantLimits::new(10 * 1024 * 1024);
255 let dir = tempfile::tempdir().unwrap();
256 let vfs = crate::runtime::Vfs::external(dir.path().to_path_buf()).unwrap();
257 let final_path = dir.path().join("out.txt");
258 let mut w = writer_over(&vfs, &limits).await;
259 w.write(&EffectId::stub().unwrap(), None, b"hello\n".to_vec())
260 .await
261 .unwrap();
262 w.close(&EffectId::stub().unwrap(), None).await.unwrap();
263 assert!(final_path.exists());
264 assert!(temp_leftovers(dir.path()).is_empty());
265 assert_eq!(limits.host_attached_bytes(), 0);
266 assert_eq!(std::fs::read_to_string(&final_path).unwrap(), "hello\n");
267 }
268}