1use super::owned::OwnedOp;
42use super::{IoUring, SubmissionToken, raw};
43use air_sys_types::Errno;
44use air_sys_types::io_uring::{TimeoutFlags, TimeoutSpec};
45use alloc::boxed::Box;
46use alloc::vec::Vec;
47
48impl IoUring {
49 pub fn link_chain(&mut self) -> LinkedChainBuilder<'_> {
55 let start_tail = self.sq.local_tail();
56 LinkedChainBuilder {
57 ring: self,
58 start_tail,
59 tokens: Vec::new(),
60 prev_index: None,
61 }
62 }
63
64 fn submit_link_timeout_staged(
68 &mut self,
69 spec: TimeoutSpec,
70 flags: TimeoutFlags,
71 ) -> Result<SubmissionToken, Errno> {
72 let ts = Box::new(super::async_ops::timespec_of(spec));
73 let ts_ptr = core::ptr::from_ref(&*ts) as u64;
74 let op_flags = flags.bits();
75 self.submit_op(Some(OwnedOp::Timeout(ts)), |sqe| {
76 sqe.opcode = raw::IORING_OP_LINK_TIMEOUT;
77 sqe.fd = -1;
78 sqe.addr_or_splice_off_in = ts_ptr;
79 sqe.len = 1;
80 sqe.op_flags = op_flags;
81 })
82 }
83}
84
85pub struct LinkedChainBuilder<'ring> {
89 ring: &'ring mut IoUring,
91 start_tail: u32,
93 tokens: Vec<SubmissionToken>,
95 prev_index: Option<u32>,
98}
99
100impl LinkedChainBuilder<'_> {
101 fn stage<F>(mut self, link_flag: Option<u8>, op: F) -> Result<Self, Errno>
105 where
106 F: FnOnce(&mut IoUring) -> Result<SubmissionToken, Errno>,
107 {
108 let index = self.ring.sq.local_tail();
109 match op(self.ring) {
110 Ok(token) => {
111 if let (Some(flag), Some(prev)) = (link_flag, self.prev_index) {
112 unsafe {
115 self.ring.sq.or_sqe_flags(prev, flag);
116 }
117 }
118 self.tokens.push(token);
119 self.prev_index = Some(index);
120 Ok(self)
121 }
122 Err(error) => {
123 self.rollback();
124 Err(error)
125 }
126 }
127 }
128
129 pub fn first<F>(self, op: F) -> Result<Self, Errno>
137 where
138 F: FnOnce(&mut IoUring) -> Result<SubmissionToken, Errno>,
139 {
140 self.stage(None, op)
141 }
142
143 pub fn then<F>(self, op: F) -> Result<Self, Errno>
151 where
152 F: FnOnce(&mut IoUring) -> Result<SubmissionToken, Errno>,
153 {
154 self.stage(Some(raw::IOSQE_IO_LINK), op)
155 }
156
157 pub fn then_hard<F>(self, op: F) -> Result<Self, Errno>
165 where
166 F: FnOnce(&mut IoUring) -> Result<SubmissionToken, Errno>,
167 {
168 self.stage(Some(raw::IOSQE_IO_HARDLINK), op)
169 }
170
171 pub fn with_link_timeout(
182 mut self,
183 spec: TimeoutSpec,
184 flags: TimeoutFlags,
185 ) -> Result<Self, Errno> {
186 let Some(prev) = self.prev_index else {
187 self.rollback();
189 return Err(Errno::EINVAL);
190 };
191 let index = self.ring.sq.local_tail();
192 match self.ring.submit_link_timeout_staged(spec, flags) {
193 Ok(token) => {
194 unsafe {
198 self.ring.sq.or_sqe_flags(prev, raw::IOSQE_IO_LINK);
199 }
200 self.tokens.push(token);
201 self.prev_index = Some(index);
202 Ok(self)
203 }
204 Err(error) => {
205 self.rollback();
206 Err(error)
207 }
208 }
209 }
210
211 pub fn submit(mut self) -> Result<ChainTokens, Errno> {
220 self.ring.submit()?;
221 Ok(ChainTokens {
222 tokens: core::mem::take(&mut self.tokens),
223 })
224 }
225
226 fn rollback(&mut self) {
229 let tokens = core::mem::take(&mut self.tokens);
230 for token in tokens {
231 self.ring.slab.release(token);
232 }
233 self.ring.sq.rewind_to(self.start_tail);
234 self.prev_index = None;
235 }
236}
237
238#[derive(Debug, Clone)]
241pub struct ChainTokens {
242 tokens: Vec<SubmissionToken>,
243}
244
245impl ChainTokens {
246 #[must_use]
248 pub fn tokens(&self) -> &[SubmissionToken] {
249 &self.tokens
250 }
251
252 #[must_use]
254 pub fn get(&self, index: usize) -> Option<SubmissionToken> {
255 self.tokens.get(index).copied()
256 }
257
258 #[must_use]
260 pub fn len(&self) -> usize {
261 self.tokens.len()
262 }
263
264 #[must_use]
266 pub fn is_empty(&self) -> bool {
267 self.tokens.is_empty()
268 }
269
270 #[must_use]
272 pub fn into_vec(self) -> Vec<SubmissionToken> {
273 self.tokens
274 }
275}
276
277#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::io_uring::{Completion, IoUring, IoUringBuilder, SubmitOptions};
291 use crate::test_support::std_file;
292 use air_sys_types::fd::AsFd;
293 use air_sys_types::io_uring::PollEvents;
294 use air_sys_types::ipc::PipeFlags;
295 use core::num::NonZeroU32;
296 use core::time::Duration;
297 use std::io::Write;
298
299 fn ring8() -> IoUring {
300 IoUring::new(NonZeroU32::new(8).expect("8 ≠ 0")).expect("io_uring_setup 6.12")
301 }
302
303 fn nz(n: u32) -> NonZeroU32 {
304 NonZeroU32::new(n).expect("n ≠ 0")
305 }
306
307 fn pipe_pair() -> (air_sys_types::fd::OwnedFd, air_sys_types::fd::OwnedFd) {
308 crate::ipc::pipe2(PipeFlags::empty()).expect("pipe2")
309 }
310
311 fn drain(ring: &mut IoUring, n: u32) -> Vec<Completion> {
314 ring.submit_and_wait(n).expect("submit_and_wait");
315 let mut out = Vec::new();
316 while out.len() < n as usize {
317 out.push(ring.wait_completion().expect("wait_completion"));
318 }
319 out
320 }
321
322 fn res_of(completions: &[Completion], tok: super::SubmissionToken) -> i32 {
324 completions
325 .iter()
326 .find(|c| c.token() == tok)
327 .map(Completion::raw_result)
328 .expect("complétion du jeton présente")
329 }
330
331 #[test]
334 fn chain_tokens_accessors() {
335 let t0 = super::SubmissionToken::new(0, 0);
336 let t1 = super::SubmissionToken::new(1, 0);
337 let tokens = ChainTokens {
338 tokens: vec![t0, t1],
339 };
340 assert_eq!(tokens.len(), 2);
341 assert!(!tokens.is_empty());
342 assert_eq!(tokens.get(0), Some(t0));
343 assert_eq!(tokens.get(1), Some(t1));
344 assert_eq!(tokens.get(2), None);
345 assert_eq!(tokens.tokens(), &[t0, t1]);
346 assert_eq!(tokens.clone().into_vec(), vec![t0, t1]);
347 let empty = ChainTokens { tokens: Vec::new() };
348 assert!(empty.is_empty());
349 assert!(format!("{tokens:?}").contains("ChainTokens"));
350 }
351
352 #[test]
355 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
356 fn chain_of_nops_completes_in_order() {
357 let mut ring = ring8();
358 let tokens = ring
359 .link_chain()
360 .first(|r| r.submit_nop())
361 .expect("first")
362 .then(|r| r.submit_nop())
363 .expect("then")
364 .then(|r| r.submit_nop())
365 .expect("then")
366 .submit()
367 .expect("submit");
368 assert_eq!(tokens.len(), 3);
369 let completions = drain(&mut ring, 3);
370 for i in 0..3 {
371 let tok = tokens.get(i).expect("jeton");
372 assert_eq!(res_of(&completions, tok), 0, "maillon {i} succès");
373 }
374 assert_eq!(ring.in_flight(), 0, "aucun slot perdu");
375 }
376
377 #[test]
380 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
381 fn soft_link_breaks_on_error_cancels_rest() {
382 let mut ring = ring8();
383 let tokens = ring
385 .link_chain()
386 .first(|r| r.submit_nop())
387 .expect("first")
388 .then(|r| r.submit_nop_with_result(-22)) .expect("then err")
390 .then(|r| r.submit_nop())
391 .expect("then")
392 .submit()
393 .expect("submit");
394 let completions = drain(&mut ring, 3);
395 assert_eq!(res_of(&completions, tokens.get(0).unwrap()), 0, "1er OK");
396 assert_eq!(
397 res_of(&completions, tokens.get(1).unwrap()),
398 -22,
399 "2e erreur"
400 );
401 assert_eq!(
402 res_of(&completions, tokens.get(2).unwrap()),
403 -125,
404 "3e annulé (-ECANCELED)"
405 );
406 assert_eq!(ring.in_flight(), 0);
407 }
408
409 #[test]
410 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
411 fn soft_link_short_read_breaks_chain() {
412 let (reader, writer) = pipe_pair();
414 {
415 let mut w = std_file(writer);
416 w.write_all(b"short").expect("write 5"); w.flush().expect("flush");
418 }
419 let mut ring = ring8();
420 let tokens = ring
422 .link_chain()
423 .first(|r| r.submit_read(reader.as_fd(), vec![0u8; 64], None))
424 .expect("first read")
425 .then(|r| r.submit_nop())
426 .expect("then")
427 .submit()
428 .expect("submit");
429 let completions = drain(&mut ring, 2);
430 let read_res = res_of(&completions, tokens.get(0).unwrap());
431 let nop_res = res_of(&completions, tokens.get(1).unwrap());
432 assert_eq!(read_res, 5, "read court : 5 octets lus");
433 assert_eq!(
434 nop_res, -125,
435 "short read rompt la chaîne soft → nop -ECANCELED (piège confirmé)"
436 );
437 assert_eq!(ring.in_flight(), 0);
438 }
439
440 #[test]
443 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
444 fn hard_link_survives_error() {
445 let mut ring = ring8();
446 let tokens = ring
449 .link_chain()
450 .first(|r| r.submit_nop_with_result(-22))
451 .expect("first err")
452 .then_hard(|r| r.submit_nop())
453 .expect("then_hard")
454 .submit()
455 .expect("submit");
456 let completions = drain(&mut ring, 2);
457 assert_eq!(
458 res_of(&completions, tokens.get(0).unwrap()),
459 -22,
460 "1er erreur"
461 );
462 assert_eq!(
463 res_of(&completions, tokens.get(1).unwrap()),
464 0,
465 "hard link : 2e exécuté malgré l'erreur"
466 );
467 assert_eq!(ring.in_flight(), 0);
468 }
469
470 #[test]
473 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
474 fn link_timeout_expires_cancels_bounded() {
475 use air_sys_types::io_uring::{TimeoutFlags, TimeoutSpec};
476 let (reader, _writer) = pipe_pair();
478 let mut ring = ring8();
479 let tokens = ring
480 .link_chain()
481 .first(|r| r.submit_poll_add(reader.as_fd(), PollEvents::IN))
482 .expect("first poll")
483 .with_link_timeout(
484 TimeoutSpec::after(Duration::from_millis(50)),
485 TimeoutFlags::empty(),
486 )
487 .expect("link_timeout")
488 .submit()
489 .expect("submit");
490 let completions = drain(&mut ring, 2);
491 let poll_res = res_of(&completions, tokens.get(0).unwrap());
492 let timeout_res = res_of(&completions, tokens.get(1).unwrap());
493 assert_eq!(poll_res, -125, "poll annulé (-ECANCELED) à l'expiration");
494 assert!(
496 timeout_res == -62 || timeout_res >= 0,
497 "timeout complète (ETIME ou succès), res = {timeout_res}"
498 );
499 assert_eq!(ring.in_flight(), 0);
500 }
501
502 #[test]
503 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
504 fn link_timeout_not_expiring_is_cancelled() {
505 use air_sys_types::io_uring::{TimeoutFlags, TimeoutSpec};
506 let mut ring = ring8();
507 let tokens = ring
509 .link_chain()
510 .first(|r| r.submit_nop())
511 .expect("first nop")
512 .with_link_timeout(
513 TimeoutSpec::after(Duration::from_secs(5)),
514 TimeoutFlags::empty(),
515 )
516 .expect("link_timeout")
517 .submit()
518 .expect("submit");
519 let completions = drain(&mut ring, 2);
520 assert_eq!(res_of(&completions, tokens.get(0).unwrap()), 0, "nop OK");
521 assert_eq!(
522 res_of(&completions, tokens.get(1).unwrap()),
523 -125,
524 "timeout annulé (maillon fini avant)"
525 );
526 assert_eq!(ring.in_flight(), 0);
527 }
528
529 #[test]
530 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
531 fn with_link_timeout_without_op_is_einval() {
532 let mut ring = ring8();
533 use air_sys_types::io_uring::{TimeoutFlags, TimeoutSpec};
534 let result = ring.link_chain().with_link_timeout(
535 TimeoutSpec::after(Duration::from_secs(1)),
536 TimeoutFlags::empty(),
537 );
538 assert!(matches!(result, Err(Errno::EINVAL)));
539 let tok = ring.submit_nop().expect("nop");
541 let _ = drain(&mut ring, 1);
542 let _ = tok;
543 assert_eq!(ring.in_flight(), 0);
544 }
545
546 #[test]
547 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
548 fn with_link_timeout_staging_error_rolls_back() {
549 use air_sys_types::io_uring::{TimeoutFlags, TimeoutSpec};
550 let mut ring = IoUringBuilder::new(nz(8))
553 .max_inflight(nz(1))
554 .build()
555 .expect("build max_inflight 1");
556 let result = ring.link_chain().first(|r| r.submit_nop()).and_then(|b| {
557 b.with_link_timeout(
558 TimeoutSpec::after(Duration::from_secs(1)),
559 TimeoutFlags::empty(),
560 )
561 });
562 assert!(
563 matches!(result, Err(Errno::EBUSY)),
564 "LINK_TIMEOUT slab plein"
565 );
566 assert_eq!(ring.in_flight(), 0, "rollback : slot du 1er maillon rendu");
567 let tok = ring.submit_nop().expect("nop après rollback");
569 ring.submit_and_wait(1).expect("submit_and_wait");
570 assert_eq!(ring.wait_completion().expect("completion").token(), tok);
571 }
572
573 #[test]
574 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
575 fn then_without_first_is_a_valid_unlinked_start() {
576 let mut ring = ring8();
579 let tokens = ring
580 .link_chain()
581 .then(|r| r.submit_nop())
582 .expect("then sans first")
583 .then(|r| r.submit_nop())
584 .expect("then")
585 .submit()
586 .expect("submit");
587 assert_eq!(tokens.len(), 2);
588 let completions = drain(&mut ring, 2);
589 assert_eq!(res_of(&completions, tokens.get(0).unwrap()), 0);
590 assert_eq!(res_of(&completions, tokens.get(1).unwrap()), 0);
591 assert_eq!(ring.in_flight(), 0);
592 }
593
594 #[test]
597 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
598 fn skip_cqe_intermediate_only_final_completes() {
599 let mut ring = ring8();
600 let skip = || SubmitOptions::default().skip_cqe_on_success();
601 let tokens = ring
602 .link_chain()
603 .first(|r| r.with(skip()).submit_nop())
604 .expect("first skip")
605 .then(|r| r.with(skip()).submit_nop())
606 .expect("then skip")
607 .then(|r| r.submit_nop())
608 .expect("then final")
609 .submit()
610 .expect("submit");
611 assert_eq!(tokens.len(), 3);
612 ring.submit_and_wait(1).expect("submit_and_wait");
614 let final_completion = ring.wait_completion().expect("finale");
615 assert_eq!(final_completion.token(), tokens.get(2).unwrap());
616 assert_eq!(final_completion.raw_result(), 0);
617 assert!(
618 ring.try_completion().is_none(),
619 "aucune complétion intermédiaire"
620 );
621 assert_eq!(
622 ring.in_flight(),
623 0,
624 "slots intermédiaires libérés au staging"
625 );
626 }
627
628 #[test]
631 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
632 fn chain_rolls_back_on_slab_exhaustion() {
633 let mut ring = IoUringBuilder::new(nz(8))
635 .max_inflight(nz(2))
636 .build()
637 .expect("build max_inflight 2");
638 let result = ring
639 .link_chain()
640 .first(|r| r.submit_nop())
641 .and_then(|b| b.then(|r| r.submit_nop()))
642 .and_then(|b| b.then(|r| r.submit_nop())); assert!(
644 matches!(result, Err(Errno::EBUSY)),
645 "EBUSY au maillon de trop"
646 );
647 assert_eq!(ring.in_flight(), 0, "rollback : slots rendus");
649 let tok = ring.submit_nop().expect("nop après rollback");
650 ring.submit_and_wait(1).expect("submit_and_wait");
651 let c = ring.wait_completion().expect("completion");
652 assert_eq!(c.token(), tok);
653 assert_eq!(ring.in_flight(), 0);
654 }
655
656 #[test]
659 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
660 fn chain_lengths_order_and_counts() {
661 for len in 1u32..=6 {
662 let mut ring = ring8();
663 let mut builder = ring.link_chain();
664 builder = builder.first(|r| r.submit_nop()).expect("first");
665 for _ in 1..len {
666 builder = builder.then(|r| r.submit_nop()).expect("then");
667 }
668 let tokens = builder.submit().expect("submit");
669 assert_eq!(u32::try_from(tokens.len()).unwrap(), len);
670 let completions = drain(&mut ring, len);
671 for i in 0..usize::try_from(len).unwrap() {
673 assert_eq!(res_of(&completions, tokens.get(i).unwrap()), 0);
674 }
675 assert_eq!(ring.in_flight(), 0);
676 }
677 }
678
679 #[test]
680 #[cfg_attr(miri, ignore = "io_uring_enter non supporté par Miri")]
681 fn first_after_then_starts_new_unlinked_segment() {
682 let mut ring = ring8();
686 let tokens = ring
687 .link_chain()
688 .first(|r| r.submit_nop())
689 .expect("first")
690 .then(|r| r.submit_nop())
691 .expect("then")
692 .first(|r| r.submit_nop()) .expect("first 2")
694 .submit()
695 .expect("submit");
696 assert_eq!(tokens.len(), 3);
697 let completions = drain(&mut ring, 3);
698 for i in 0..3 {
699 assert_eq!(res_of(&completions, tokens.get(i).unwrap()), 0);
700 }
701 assert_eq!(ring.in_flight(), 0);
702 }
703}