Skip to main content

hrx/storage/
mod.rs

1//! Bounded GPU-authored file I/O over registered system memory.
2//!
3//! This is not a peer-to-peer VRAM or durable-checkpoint API. Files, native
4//! commands and payload slots remain owned through I/O and downstream copies.
5use crate::{Error, Result, fabric};
6pub use fabric::StorageProgress;
7use std::{
8    collections::VecDeque,
9    fs::File,
10    os::{
11        fd::AsRawFd,
12        unix::fs::{MetadataExt, OpenOptionsExt},
13    },
14    sync::{
15        Arc, Condvar, Mutex, Weak,
16        atomic::{AtomicU64, Ordering},
17    },
18    time::{Duration, Instant},
19};
20
21/// Explicit filesystem I/O policy. Unsupported direct I/O never becomes buffered I/O.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum StorageMode {
24    /// Ordinary filesystem I/O, including the page cache.
25    #[default]
26    Buffered,
27    /// Filesystem-aligned direct I/O into registered system pages.
28    Direct,
29}
30/// Capacity fixed before native resources or a service thread are created.
31#[derive(Clone, Debug)]
32pub struct StorageConfig {
33    /// Filesystem route.
34    pub mode: StorageMode,
35    /// Native kernel progress service.
36    pub progress: StorageProgress,
37    /// Number of simultaneously retained payloads (1 through 64).
38    pub slots: usize,
39    /// Maximum physical I/O bytes per slot, including alignment padding.
40    pub slot_bytes: usize,
41    /// Collect host-observed timing and service counters.
42    pub statistics: bool,
43}
44impl Default for StorageConfig {
45    fn default() -> Self {
46        Self {
47            mode: StorageMode::Buffered,
48            progress: StorageProgress::Sqpoll,
49            slots: 4,
50            slot_bytes: 16 << 20,
51            statistics: false,
52        }
53    }
54}
55/// Measured session counters. Durations use host clocks, not GPU timestamps.
56#[derive(Clone, Debug, Default)]
57pub struct StorageStatistics {
58    /// Accepted logical requests, including shared reads.
59    pub logical_requests: u64,
60    /// Native requests submitted to the kernel.
61    pub physical_requests: u64,
62    /// Logical reads sharing an existing retained read.
63    pub shared_reads: u64,
64    /// Bytes from successful requests, including alignment padding.
65    pub completed_bytes: u64,
66    /// Distinct requests ending in an error; shared tickets count once.
67    pub errors: u64,
68    /// Maximum simultaneously retained slots.
69    pub peak_slots: usize,
70    /// Largest submitted batch (not NVMe hardware queue depth).
71    pub peak_outstanding: usize,
72    /// Accumulated queue delay before GPU publication, when enabled.
73    pub admission_time: Option<Duration>,
74    /// Accumulated submit-to-observed-completion time, when enabled.
75    pub ready_time: Option<Duration>,
76    /// Service thread CPU time, when supported and enabled.
77    pub service_cpu_time: Option<Duration>,
78    /// Kernel enter calls: SQPOLL wakes or deferred-work service.
79    pub service_calls: u64,
80    /// Bounded eventfd waits.
81    pub wait_calls: u64,
82}
83struct RegisteredFile {
84    file: File,
85    bytes: AtomicU64,
86    alignment: usize,
87    writable: bool,
88    readable: bool,
89    identity: (u64, u64),
90}
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92struct Extent {
93    file: usize,
94    offset: u64,
95    bytes: usize,
96    writing: bool,
97}
98#[derive(Clone, Copy, Debug)]
99struct Physical {
100    offset: u64,
101    bytes: usize,
102    skip: usize,
103}
104fn physical(extent: Extent, size: u64, alignment: usize, capacity: usize) -> Result<Physical> {
105    let end = extent
106        .offset
107        .checked_add(extent.bytes as u64)
108        .ok_or_else(|| Error::Message("file extent overflow".into()))?;
109    if extent.bytes == 0 || extent.bytes > capacity || (!extent.writing && end > size) {
110        return Err(Error::Message(
111            "file extent exceeds its file or payload slot".into(),
112        ));
113    }
114    let mask = alignment as u64 - 1;
115    if extent.writing && (extent.offset & mask != 0 || extent.bytes & (alignment - 1) != 0) {
116        return Err(Error::Message(
117            "direct writes require aligned file offsets and lengths".into(),
118        ));
119    }
120    let offset = extent.offset & !mask;
121    let end = end
122        .checked_add(mask)
123        .ok_or_else(|| Error::Message("aligned extent overflow".into()))?
124        & !mask;
125    let bytes = usize::try_from(end - offset)
126        .map_err(|_| Error::Message("file extent too large".into()))?;
127    if bytes > capacity {
128        return Err(Error::Message("aligned read exceeds payload slot".into()));
129    }
130    Ok(Physical {
131        offset,
132        bytes,
133        skip: (extent.offset - offset) as usize,
134    })
135}
136fn register(file: &File, mode: StorageMode) -> Result<RegisteredFile> {
137    let metadata = file.metadata()?;
138    if !metadata.is_file() {
139        return Err(Error::Unsupported("storage requires a regular file".into()));
140    }
141    let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
142    if flags < 0 {
143        return Err(std::io::Error::last_os_error().into());
144    }
145    let access = flags & libc::O_ACCMODE;
146    if flags & libc::O_APPEND != 0 {
147        return Err(Error::Unsupported(
148            "storage does not accept append-only descriptors".into(),
149        ));
150    }
151    let file = if mode == StorageMode::Direct || flags & libc::O_DIRECT != 0 {
152        let opened = std::fs::OpenOptions::new()
153            .read(access != libc::O_WRONLY)
154            .write(access != libc::O_RDONLY)
155            .custom_flags(
156                libc::O_CLOEXEC
157                    | (flags & (libc::O_SYNC | libc::O_DSYNC))
158                    | if mode == StorageMode::Direct {
159                        libc::O_DIRECT
160                    } else {
161                        0
162                    },
163            )
164            .open(format!("/proc/self/fd/{}", file.as_raw_fd()))?;
165        let actual = opened.metadata()?;
166        if (actual.dev(), actual.ino()) != (metadata.dev(), metadata.ino()) {
167            return Err(Error::Message("storage file identity changed".into()));
168        }
169        opened
170    } else {
171        file.try_clone()?
172    };
173    let alignment = if mode == StorageMode::Direct {
174        let mut info: libc::statx = unsafe { std::mem::zeroed() };
175        let result = unsafe {
176            libc::statx(
177                file.as_raw_fd(),
178                c"".as_ptr(),
179                libc::AT_EMPTY_PATH,
180                libc::STATX_DIOALIGN,
181                &mut info,
182            )
183        };
184        if result != 0
185            || info.stx_mask & libc::STATX_DIOALIGN == 0
186            || info.stx_dio_mem_align == 0
187            || info.stx_dio_offset_align == 0
188        {
189            return Err(Error::Unsupported(
190                "filesystem does not report direct-I/O alignment".into(),
191            ));
192        }
193        let alignment = info.stx_dio_mem_align.max(info.stx_dio_offset_align) as usize;
194        let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
195        if !info.stx_dio_mem_align.is_power_of_two()
196            || !info.stx_dio_offset_align.is_power_of_two()
197            || alignment > page
198        {
199            return Err(Error::Unsupported(
200                "direct-I/O alignment exceeds registered-page alignment".into(),
201            ));
202        }
203        alignment
204    } else {
205        1
206    };
207    Ok(RegisteredFile {
208        file,
209        bytes: AtomicU64::new(metadata.len()),
210        alignment,
211        writable: access != libc::O_RDONLY,
212        readable: access != libc::O_WRONLY && flags & libc::O_PATH == 0,
213        identity: (metadata.dev(), metadata.ino()),
214    })
215}
216type Outcome = std::result::Result<usize, Arc<Error>>;
217struct Request {
218    shared: Arc<Shared>,
219    extent: Extent,
220    physical: Physical,
221    slot: usize,
222    submitted: Instant,
223    outcome: Mutex<Option<Outcome>>,
224    ready: Condvar,
225}
226struct State {
227    pending: VecDeque<Arc<Request>>,
228    slots: Vec<Weak<Request>>,
229    stop: bool,
230    failed: Option<Arc<Error>>,
231    statistics: StorageStatistics,
232}
233struct Shared {
234    state: Mutex<State>,
235    changed: Condvar,
236    payload: fabric::Buffer,
237    files: Vec<RegisteredFile>,
238    config: StorageConfig,
239    device: fabric::Device,
240}
241fn stop_after_failure(shared: &Shared, error: Error) -> Error {
242    let error = Arc::new(error);
243    let mut state = shared.state.lock().unwrap();
244    state.failed.get_or_insert_with(|| error.clone());
245    state.stop = true;
246    for request in &state.pending {
247        *request.outcome.lock().unwrap() = Some(Err(error.clone()));
248        request.ready.notify_all();
249    }
250    state.statistics.errors += state.pending.len() as u64;
251    state.pending.clear();
252    shared.changed.notify_all();
253    Error::Execution { source: error }
254}
255/// Reusable storage queue with bounded payload ownership and one service thread.
256/// Opening a session requires a stream in the process-lifetime native domain.
257pub struct StorageSession {
258    shared: Arc<Shared>,
259    worker: Option<std::thread::JoinHandle<()>>,
260}
261impl StorageSession {
262    /// Register files and allocate all payload slots before accepting requests.
263    pub fn new(stream: &crate::Stream, files: &[File], config: StorageConfig) -> Result<Self> {
264        let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
265        if page <= 0 {
266            return Err(Error::Unsupported("host page size unavailable".into()));
267        }
268        if config.slots == 0
269            || config.slots > 64
270            || config.slot_bytes == 0
271            || config.slot_bytes > 64 << 20
272            || !config.slot_bytes.is_multiple_of(page as usize)
273        {
274            return Err(Error::Message(
275                "storage needs 1..=64 slots of page-aligned size, at most 64 MiB each".into(),
276            ));
277        }
278        let files = files
279            .iter()
280            .map(|f| register(f, config.mode))
281            .collect::<Result<Vec<_>>>()?;
282        if files.is_empty() || files.len() > 1024 {
283            return Err(Error::Message("storage needs 1..=1024 files".into()));
284        }
285        let device = stream.native_device().clone();
286        let payload = device.fabric().allocate_registered(
287            config
288                .slot_bytes
289                .checked_mul(config.slots)
290                .ok_or_else(|| Error::Message("storage capacity overflow".into()))?,
291            std::slice::from_ref(&device),
292            stream.memory_budget(),
293        )?;
294        let shared = Arc::new(Shared {
295            state: Mutex::new(State {
296                pending: VecDeque::with_capacity(config.slots),
297                slots: (0..config.slots).map(|_| Weak::new()).collect(),
298                stop: false,
299                failed: None,
300                statistics: StorageStatistics::default(),
301            }),
302            changed: Condvar::new(),
303            payload,
304            files,
305            config,
306            device,
307        });
308        let budget = stream.memory_budget().cloned();
309        let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
310        let context = shared.clone();
311        let worker = std::thread::Builder::new()
312            .name("hrx-storage".into())
313            .spawn(move || {
314                let prepared = Worker::new(&context, budget);
315                match prepared {
316                    Ok(mut worker) => {
317                        let _ = ready_tx.send(Ok(()));
318                        worker.run(context);
319                    }
320                    Err(error) => {
321                        let _ = ready_tx.send(Err(error));
322                    }
323                }
324            })?;
325        match ready_rx
326            .recv()
327            .map_err(|_| Error::DeviceLost("storage setup worker stopped".into()))?
328        {
329            Ok(()) => Ok(Self {
330                shared,
331                worker: Some(worker),
332            }),
333            Err(error) => {
334                let _ = worker.join();
335                Err(error)
336            }
337        }
338    }
339    /// Maximum logical read bytes guaranteed to fit despite direct-I/O padding.
340    pub fn read_chunk_bytes(&self) -> usize {
341        self.shared.config.slot_bytes.saturating_sub(
342            self.shared
343                .files
344                .iter()
345                .map(|f| f.alignment)
346                .max()
347                .unwrap_or(1)
348                - 1,
349        )
350    }
351    /// Read an exact immutable extent. Identical retained reads share one slot.
352    /// Files must not be written through another handle while a shared read lives.
353    pub fn read(&self, file: usize, offset: u64, bytes: usize) -> Result<ReadTicket> {
354        Ok(ReadTicket {
355            request: self.submit(
356                Extent {
357                    file,
358                    offset,
359                    bytes,
360                    writing: false,
361                },
362                None,
363            )?,
364        })
365    }
366    /// Copy host bytes into an owned slot and submit a positioned write.
367    /// Completion reports transferred bytes, not power-loss durability.
368    /// Returns Busy when another slot has active device use: host writes obey
369    /// the registered allocation's whole-buffer access exclusion.
370    pub fn write(&self, file: usize, offset: u64, bytes: &[u8]) -> Result<ReadTicket> {
371        Ok(ReadTicket {
372            request: self.submit(
373                Extent {
374                    file,
375                    offset,
376                    bytes: bytes.len(),
377                    writing: true,
378                },
379                Some(bytes),
380            )?,
381        })
382    }
383    fn submit(&self, extent: Extent, data: Option<&[u8]>) -> Result<Arc<Request>> {
384        let file = self
385            .shared
386            .files
387            .get(extent.file)
388            .ok_or_else(|| Error::Message("unregistered file index".into()))?;
389        if (extent.writing && !file.writable) || (!extent.writing && !file.readable) {
390            return Err(std::io::Error::from_raw_os_error(libc::EBADF).into());
391        }
392        let physical = physical(
393            extent,
394            file.bytes.load(Ordering::Acquire),
395            file.alignment,
396            self.shared.config.slot_bytes,
397        )?;
398        let mut state = self.shared.state.lock().unwrap();
399        if let Some(error) = &state.failed {
400            return Err(Error::Execution {
401                source: error.clone(),
402            });
403        }
404        if state.stop {
405            return Err(Error::Cancelled);
406        }
407        for request in state.slots.iter().filter_map(Weak::upgrade) {
408            if self.shared.files[request.extent.file].identity == file.identity {
409                let other = request.extent;
410                if !extent.writing
411                    && !other.writing
412                    && other.offset == extent.offset
413                    && other.bytes == extent.bytes
414                    && !matches!(*request.outcome.lock().unwrap(), Some(Err(_)))
415                {
416                    state.statistics.logical_requests += 1;
417                    state.statistics.shared_reads += 1;
418                    return Ok(request);
419                }
420                if (extent.writing || other.writing)
421                    && extent.offset < other.offset + other.bytes as u64
422                    && other.offset < extent.offset + extent.bytes as u64
423                {
424                    return Err(Error::Busy(
425                        "overlapping storage write is still retained".into(),
426                    ));
427                }
428            }
429        }
430        let slot = state
431            .slots
432            .iter()
433            .position(|s| s.strong_count() == 0)
434            .ok_or_else(|| Error::Busy("all storage payload slots are retained".into()))?;
435        if let Some(data) = data {
436            self.shared
437                .payload
438                .write(slot * self.shared.config.slot_bytes, data)?;
439        }
440        let request = Arc::new(Request {
441            shared: self.shared.clone(),
442            extent,
443            physical,
444            slot,
445            submitted: Instant::now(),
446            outcome: Mutex::new(None),
447            ready: Condvar::new(),
448        });
449        state.slots[slot] = Arc::downgrade(&request);
450        state.pending.push_back(request.clone());
451        state.statistics.logical_requests += 1;
452        state.statistics.peak_slots = state
453            .statistics
454            .peak_slots
455            .max(state.slots.iter().filter(|s| s.strong_count() > 0).count());
456        self.shared.changed.notify_one();
457        Ok(request)
458    }
459    /// Snapshot counters without waiting for outstanding operations.
460    pub fn statistics(&self) -> StorageStatistics {
461        self.shared.state.lock().unwrap().statistics.clone()
462    }
463}
464impl Drop for StorageSession {
465    fn drop(&mut self) {
466        self.shared.state.lock().unwrap().stop = true;
467        self.shared.changed.notify_all();
468        if let Some(worker) = self.worker.take() {
469            let _ = worker.join();
470        }
471    }
472}
473/// A retained asynchronous read or write. Dropping it never cancels accepted I/O.
474#[derive(Clone)]
475pub struct ReadTicket {
476    request: Arc<Request>,
477}
478impl ReadTicket {
479    /// Observe completion without releasing the payload slot.
480    pub fn is_complete(&self) -> bool {
481        self.request.outcome.lock().unwrap().is_some()
482    }
483    /// Wait under one deadline. A timeout keeps the request and slot alive.
484    pub fn wait_timeout(&self, timeout: Duration) -> Result<Option<ReadLease>> {
485        let state = self.request.outcome.lock().unwrap();
486        let (state, _) = self
487            .request
488            .ready
489            .wait_timeout_while(state, timeout, |s| s.is_none())
490            .unwrap();
491        match state.as_ref() {
492            None => Ok(None),
493            Some(Err(error)) => Err(Error::Execution {
494                source: error.clone(),
495            }),
496            Some(Ok(_)) => Ok(Some(ReadLease {
497                request: self.request.clone(),
498            })),
499        }
500    }
501    /// Wait for a completed exact extent.
502    pub fn wait(&self) -> Result<ReadLease> {
503        loop {
504            if let Some(lease) = self.wait_timeout(Duration::from_secs(1))? {
505                return Ok(lease);
506            }
507        }
508    }
509}
510/// Completed payload, retained until every reader and copy ticket releases it.
511#[derive(Clone)]
512pub struct ReadLease {
513    request: Arc<Request>,
514}
515impl ReadLease {
516    /// Logical extent, excluding direct-I/O padding.
517    pub fn len(&self) -> usize {
518        self.request.extent.bytes
519    }
520    /// Whether this extent is empty (zero-length submissions are rejected).
521    pub fn is_empty(&self) -> bool {
522        self.len() == 0
523    }
524    /// Copy completed bytes to a caller's host slice. May return Busy while
525    /// another slot in the registered payload has outstanding device use.
526    pub fn read(&self, bytes: &mut [u8]) -> Result<()> {
527        if bytes.len() != self.len() {
528            return Err(Error::Message("storage readback length mismatch".into()));
529        }
530        self.request.shared.payload.read(self.offset(), bytes)
531    }
532    fn offset(&self) -> usize {
533        self.request.slot * self.request.shared.config.slot_bytes + self.request.physical.skip
534    }
535    /// Copy into this stream's allocation, retaining the slot through completion.
536    pub fn copy_to(
537        self,
538        stream: &mut crate::Stream,
539        destination: crate::View<'_>,
540    ) -> Result<StorageTransfer> {
541        let destination = destination.slice(0, self.len())?;
542        // SAFETY: only an immediate copy is submitted; no source binding escapes.
543        unsafe { self.enqueue(stream, |stream, source| stream.copy(destination, source)) }
544    }
545    /// Submit an immediate consumer of this payload on the supplied stream.
546    /// The returned transfer retains the slot until all submitted work retires.
547    ///
548    /// # Safety
549    /// The operation must only read the source, must submit all source uses on
550    /// this stream before returning, and must not retain source bindings in a
551    /// graph or another queue for later execution.
552    pub unsafe fn enqueue(
553        self,
554        stream: &mut crate::Stream,
555        operation: impl FnOnce(&mut crate::Stream, crate::View<'_>) -> Result<()>,
556    ) -> Result<StorageTransfer> {
557        if stream.device_id() != self.request.shared.device.id() {
558            return Err(Error::Message(
559                "storage and stream belong to different native domains".into(),
560            ));
561        }
562        let buffer = stream.storage_buffer(self.request.shared.payload.clone())?;
563        let source = buffer.try_slice(self.offset(), self.len())?;
564        let submitted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
565            operation(stream, source).and_then(|()| stream.record_event())
566        }));
567        let submitted = match submitted {
568            Ok(result) => result,
569            Err(panic) => {
570                if let Err(failure) = stream.synchronize() {
571                    stop_after_failure(&self.request.shared, failure);
572                    std::mem::forget((self, buffer));
573                }
574                std::panic::resume_unwind(panic);
575            }
576        };
577        let event = match submitted {
578            Ok(event) => event,
579            Err(error) => {
580                if let Err(failure) = stream.synchronize() {
581                    stop_after_failure(&self.request.shared, failure);
582                    std::mem::forget((self, buffer));
583                }
584                return Err(error);
585            }
586        };
587        Ok(StorageTransfer {
588            retained: Some((self, buffer)),
589            event,
590        })
591    }
592}
593/// Source ownership for an asynchronous stream copy.
594pub struct StorageTransfer {
595    retained: Option<(ReadLease, crate::Buffer)>,
596    event: crate::Event,
597}
598impl StorageTransfer {
599    fn fail(&self, error: Error) -> Error {
600        match &self.retained {
601            Some((lease, _)) => stop_after_failure(&lease.request.shared, error),
602            None => error,
603        }
604    }
605    /// Reclaim the source only after the stream copy has completed.
606    pub fn is_complete(&mut self) -> Result<bool> {
607        let complete = self.event.is_complete().map_err(|error| self.fail(error))?;
608        if complete {
609            self.retained.take();
610            Ok(true)
611        } else {
612            Ok(false)
613        }
614    }
615    /// Wait and release source capacity; failures keep it retained.
616    pub fn wait(&mut self) -> Result<()> {
617        self.event.synchronize().map_err(|error| self.fail(error))?;
618        self.retained.take();
619        Ok(())
620    }
621}
622impl Drop for StorageTransfer {
623    fn drop(&mut self) {
624        if self.retained.is_some() && self.wait().is_err() {
625            std::mem::forget(self.retained.take());
626        }
627    }
628}
629struct Worker {
630    ring: Option<fabric::StorageRing>,
631    descriptors: fabric::Buffer,
632    results: fabric::Buffer,
633    kernel: fabric::Kernel,
634    queue: fabric::Queue,
635    budget: Option<crate::residency::MemoryBudget>,
636    requests: u64,
637}
638impl Worker {
639    fn new(shared: &Shared, budget: Option<crate::residency::MemoryBudget>) -> Result<Self> {
640        let files = shared
641            .files
642            .iter()
643            .map(|f| f.file.try_clone())
644            .collect::<std::io::Result<Vec<_>>>()?;
645        let ring = fabric::StorageRing::new(
646            &shared.device,
647            &files,
648            &shared.payload,
649            fabric::StorageOptions {
650                entries: 64,
651                progress: shared.config.progress,
652                memory_budget: budget.clone(),
653                ..Default::default()
654            },
655        )?;
656        let allocate = |bytes| {
657            shared.device.fabric().allocate_registered(
658                bytes,
659                std::slice::from_ref(&shared.device),
660                budget.as_ref(),
661            )
662        };
663        let descriptors = allocate(64 * 32)?;
664        let results = allocate(64 * 4)?;
665        let artifact = crate::loom::Compiler::for_target(None, shared.device.target())?
666            .module(include_str!("batch.loom"))
667            .compile(&crate::loom::Specialization::new("storage_batch"))?;
668        let kernel = unsafe { shared.device.load(&artifact) }?;
669        let queue = shared.device.queue()?;
670        Ok(Self {
671            ring: Some(ring),
672            descriptors,
673            results,
674            kernel,
675            queue,
676            budget,
677            requests: 0,
678        })
679    }
680    fn run(&mut self, shared: Arc<Shared>) {
681        loop {
682            let jobs = {
683                let mut state = shared.state.lock().unwrap();
684                while state.pending.is_empty() && !state.stop {
685                    state = shared.changed.wait(state).unwrap();
686                }
687                if state.pending.is_empty() {
688                    return;
689                }
690                state.pending.drain(..).collect::<Vec<_>>()
691            };
692            let cpu_before = thread_cpu();
693            let started = Instant::now();
694            let outcome = self.execute(&shared, &jobs);
695            let mut state = shared.state.lock().unwrap();
696            state.statistics.physical_requests = self.requests;
697            state.statistics.peak_outstanding = state.statistics.peak_outstanding.max(jobs.len());
698            if shared.config.statistics {
699                let add = |v: &mut Option<Duration>, d| *v = Some(v.unwrap_or_default() + d);
700                add(
701                    &mut state.statistics.admission_time,
702                    jobs.iter()
703                        .map(|j| started.duration_since(j.submitted))
704                        .sum(),
705                );
706                add(
707                    &mut state.statistics.ready_time,
708                    jobs.iter().map(|j| j.submitted.elapsed()).sum(),
709                );
710                if let (Some(a), Some(b)) = (cpu_before, thread_cpu()) {
711                    add(&mut state.statistics.service_cpu_time, b.saturating_sub(a));
712                }
713            }
714            if let Some(ring) = &self.ring {
715                let (service, wait) = ring.service_counts();
716                state.statistics.service_calls = service as u64;
717                state.statistics.wait_calls = wait as u64;
718            }
719            match outcome {
720                Ok(results) => {
721                    for (job, result) in jobs.iter().zip(results) {
722                        let required = job.physical.skip + job.extent.bytes;
723                        let outcome = if result < 0 {
724                            Err(Arc::new(Error::Io(std::io::Error::from_raw_os_error(
725                                -result,
726                            ))))
727                        } else if (result as usize) < required {
728                            Err(Arc::new(Error::Io(std::io::Error::new(
729                                std::io::ErrorKind::UnexpectedEof,
730                                "incomplete storage extent",
731                            ))))
732                        } else {
733                            state.statistics.completed_bytes += result as u64;
734                            if job.extent.writing {
735                                let identity = shared.files[job.extent.file].identity;
736                                for file in
737                                    shared.files.iter().filter(|file| file.identity == identity)
738                                {
739                                    file.bytes.fetch_max(
740                                        job.extent.offset + job.extent.bytes as u64,
741                                        Ordering::Release,
742                                    );
743                                }
744                            }
745                            Ok(result as usize)
746                        };
747                        if let Err(error) = &outcome {
748                            state.statistics.errors += 1;
749                            state.failed.get_or_insert_with(|| error.clone());
750                            state.stop = true;
751                        }
752                        *job.outcome.lock().unwrap() = Some(outcome);
753                        job.ready.notify_all();
754                    }
755                }
756                Err(error) => {
757                    let error = Arc::new(error);
758                    state.failed = Some(error.clone());
759                    state.stop = true;
760                    for job in jobs.iter().chain(state.pending.iter()) {
761                        *job.outcome.lock().unwrap() = Some(Err(error.clone()));
762                        job.ready.notify_all();
763                    }
764                    state.statistics.errors += (jobs.len() + state.pending.len()) as u64;
765                    state.pending.clear();
766                    return;
767                }
768            }
769            if let Some(error) = state.failed.clone() {
770                for job in &state.pending {
771                    *job.outcome.lock().unwrap() = Some(Err(error.clone()));
772                    job.ready.notify_all();
773                }
774                state.statistics.errors += state.pending.len() as u64;
775                state.pending.clear();
776                return;
777            }
778            // Completion wakes consumers before this scope ends. Release service
779            // references under the admission lock so an immediately following
780            // request sees only real tickets/leases, not a retiring batch owner.
781            drop(jobs);
782            drop(state);
783        }
784    }
785    fn execute(&mut self, shared: &Shared, jobs: &[Arc<Request>]) -> Result<Vec<i32>> {
786        let mut completed = vec![0usize; jobs.len()];
787        let mut results = vec![0i32; jobs.len()];
788        let mut remaining: Vec<usize> = (0..jobs.len()).collect();
789        while !remaining.is_empty() {
790            let attempts: Vec<_> = remaining
791                .iter()
792                .map(|&i| (&jobs[i], completed[i]))
793                .collect();
794            let returned = self.execute_batch(shared, &attempts)?;
795            let mut retry = Vec::new();
796            let mut failed = false;
797            for (i, result) in remaining.into_iter().zip(returned) {
798                let job = &jobs[i];
799                if result < 0 {
800                    results[i] = result;
801                    failed = true;
802                    continue;
803                }
804                completed[i] += result as usize;
805                results[i] = completed[i] as i32;
806                if completed[i] >= job.physical.skip + job.extent.bytes {
807                    continue;
808                }
809                // A short direct completion can only be retried at a legal boundary.
810                if result == 0
811                    || !completed[i].is_multiple_of(shared.files[job.extent.file].alignment)
812                {
813                    results[i] = if job.extent.writing {
814                        -libc::EIO
815                    } else {
816                        results[i]
817                    };
818                    failed = true;
819                } else {
820                    retry.push(i);
821                }
822            }
823            // All accepted SQEs have retired. Do not issue retries after any failure.
824            if failed {
825                break;
826            }
827            remaining = retry;
828        }
829        Ok(results)
830    }
831    fn execute_batch(
832        &mut self,
833        shared: &Shared,
834        jobs: &[(&Arc<Request>, usize)],
835    ) -> Result<Vec<i32>> {
836        let mut descriptors = vec![0u8; jobs.len() * 32];
837        for (bytes, (job, completed)) in descriptors.as_chunks_mut::<32>().0.iter_mut().zip(jobs) {
838            bytes[0..8].copy_from_slice(&(job.physical.offset + *completed as u64).to_le_bytes());
839            bytes[8..16].copy_from_slice(
840                &((job.slot * shared.config.slot_bytes + completed) as u64).to_le_bytes(),
841            );
842            bytes[16..20].copy_from_slice(&(job.extent.file as u32).to_le_bytes());
843            bytes[20..24]
844                .copy_from_slice(&(if job.extent.writing { 5u32 } else { 4u32 }).to_le_bytes());
845            bytes[24..28].copy_from_slice(&((job.physical.bytes - completed) as u32).to_le_bytes());
846        }
847        self.descriptors.write(0, &descriptors)?;
848        let ring = self.ring.as_ref().unwrap();
849        let layout = ring.layout();
850        let host = layout.host_payload.to_le_bytes();
851        let sq = layout.submission_mask.to_le_bytes();
852        let cq = layout.completion_mask.to_le_bytes();
853        let count = (jobs.len() as u32).to_le_bytes();
854        use fabric::Argument::{Buffer, Value};
855        let args = [
856            Buffer(ring.memory(), layout.submission_entries),
857            Buffer(ring.memory(), layout.submission_tail),
858            Buffer(ring.memory(), layout.completion_entries),
859            Buffer(ring.memory(), layout.completion_head),
860            Buffer(ring.memory(), layout.completion_tail),
861            Buffer(&self.descriptors, 0),
862            Buffer(&self.results, 0),
863            Value(&host),
864            Value(&sq),
865            Value(&cq),
866            Value(&count),
867        ];
868        let command = unsafe { self.queue.prepare(&self.kernel, [1; 3], [1; 3], &args) }?;
869        let charge = self
870            .budget
871            .as_ref()
872            .map(|b| b.reserve(command.storage_bytes()))
873            .transpose()?;
874        let mut execution = unsafe { self.ring.take().unwrap().dispatch(command) }?;
875        self.requests += jobs.len() as u64;
876        match execution.wait_timeout(Duration::from_secs(30)) {
877            Ok(true) => {}
878            outcome => {
879                // Unretired native allocations and their accounting stay together.
880                std::mem::forget(charge);
881                std::mem::forget(execution);
882                return Err(outcome.err().unwrap_or_else(|| {
883                    Error::DeviceLost("storage did not retire within 30 seconds".into())
884                }));
885            }
886        }
887        self.ring = Some(execution.into_ring()?);
888        let mut bytes = vec![0; jobs.len() * 4];
889        self.results.read(0, &mut bytes)?;
890        Ok(bytes
891            .as_chunks::<4>()
892            .0
893            .iter()
894            .map(|v| i32::from_le_bytes(*v))
895            .collect())
896    }
897}
898fn thread_cpu() -> Option<Duration> {
899    let mut value = libc::timespec {
900        tv_sec: 0,
901        tv_nsec: 0,
902    };
903    (unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut value) } == 0)
904        .then(|| Duration::new(value.tv_sec as u64, value.tv_nsec as u32))
905}
906#[cfg(test)]
907mod tests {
908    use super::*;
909    #[test]
910    fn aligned_extents_preserve_logical_boundaries() {
911        let read = Extent {
912            file: 0,
913            offset: 4103,
914            bytes: 700,
915            writing: false,
916        };
917        let p = physical(read, 9000, 4096, 8192).unwrap();
918        assert_eq!((p.offset, p.bytes, p.skip), (4096, 4096, 7));
919        assert!(physical(read, 4200, 4096, 8192).is_err());
920        assert!(
921            physical(
922                Extent {
923                    offset: u64::MAX,
924                    ..read
925                },
926                u64::MAX,
927                4096,
928                8192
929            )
930            .is_err()
931        );
932        assert!(
933            physical(
934                Extent {
935                    writing: true,
936                    ..read
937                },
938                9000,
939                4096,
940                8192
941            )
942            .is_err()
943        );
944        assert!(
945            physical(
946                Extent {
947                    offset: 1,
948                    bytes: 8192,
949                    ..read
950                },
951                9000,
952                4096,
953                8192
954            )
955            .is_err()
956        );
957    }
958}