1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum StorageMode {
24 #[default]
26 Buffered,
27 Direct,
29}
30#[derive(Clone, Debug)]
32pub struct StorageConfig {
33 pub mode: StorageMode,
35 pub progress: StorageProgress,
37 pub slots: usize,
39 pub slot_bytes: usize,
41 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#[derive(Clone, Debug, Default)]
57pub struct StorageStatistics {
58 pub logical_requests: u64,
60 pub physical_requests: u64,
62 pub shared_reads: u64,
64 pub completed_bytes: u64,
66 pub errors: u64,
68 pub peak_slots: usize,
70 pub peak_outstanding: usize,
72 pub admission_time: Option<Duration>,
74 pub ready_time: Option<Duration>,
76 pub service_cpu_time: Option<Duration>,
78 pub service_calls: u64,
80 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}
255pub struct StorageSession {
258 shared: Arc<Shared>,
259 worker: Option<std::thread::JoinHandle<()>>,
260}
261impl StorageSession {
262 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 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 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 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 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#[derive(Clone)]
475pub struct ReadTicket {
476 request: Arc<Request>,
477}
478impl ReadTicket {
479 pub fn is_complete(&self) -> bool {
481 self.request.outcome.lock().unwrap().is_some()
482 }
483 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 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#[derive(Clone)]
512pub struct ReadLease {
513 request: Arc<Request>,
514}
515impl ReadLease {
516 pub fn len(&self) -> usize {
518 self.request.extent.bytes
519 }
520 pub fn is_empty(&self) -> bool {
522 self.len() == 0
523 }
524 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 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 unsafe { self.enqueue(stream, |stream, source| stream.copy(destination, source)) }
544 }
545 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}
593pub 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 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 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 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 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 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 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}