Skip to main content

wireshift_core/
ring.rs

1//! Ring manager and completion routing.
2//!
3//! This module defines [`Ring`], the primary interface for submitting operations
4//! and receiving completions, as well as [`ChainContext`] for dependent operation
5//! execution.
6//!
7//! # Ring Lifecycle
8//!
9//! ```text
10//! 1. Create: Ring::new(config) -> Result<Ring>
11//!    - Validates configuration
12//!    - Creates backend via BackendFactory
13//!    - Spawns backend worker threads if needed
14//!
15//! 2. Submit: ring.submit(op) -> Result<Request<Output>>
16//!    - Assigns unique request id
17//!    - Converts operation to descriptor
18//!    - Sends to backend
19//!
20//! 3. Complete: request.wait(timeout) -> Result<Output>
21//!    - Waits for backend completion
22//!    - Maps completion payload to typed output
23//!
24//! 4. Shutdown: (implicit on Drop)
25//!    - Signals backend to shut down
26//!    - Waits for in-flight operations
27//!    - Joins worker threads
28//! ```
29//!
30//! # Fork Safety
31//!
32//! Rings are **not fork-safe**. The ring records the process ID at creation
33//! and will return an error if used from a forked child process. Create a new
34//! ring after forking.
35
36use std::borrow::Cow;
37use std::collections::VecDeque;
38use std::marker::PhantomData;
39use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
40use std::sync::mpsc::{self, Receiver, Sender};
41use std::sync::{Arc, Mutex};
42use std::time::Duration;
43
44use crate::backend::{
45    AnyBackend, Backend, BackendCompletion, BackendFactory, BackendKind, BackendSubmission,
46    CancellationHandle,
47};
48use crate::completion::{CompletionEvent, CompletionKind, Request};
49use crate::config::RingConfig;
50use crate::error::{Error, Result};
51use crate::op::{boxed_mapper, BoxedMapper, Op};
52
53pub(crate) struct InflightEntry {
54    id: u64,
55    op_name: &'static str,
56    mapper: BoxedMapper,
57    responder: Sender<Result<Box<dyn std::any::Any + Send>>>,
58}
59
60struct InflightTable {
61    slots: Vec<Option<InflightEntry>>,
62    generations: Vec<u64>,
63}
64
65impl InflightTable {
66    fn new(capacity: usize) -> Self {
67        Self {
68            slots: (0..capacity).map(|_| None).collect(),
69            generations: vec![0; capacity],
70        }
71    }
72
73    fn insert(
74        &mut self,
75        start_slot: usize,
76        op_name: &'static str,
77        mapper: BoxedMapper,
78        responder: Sender<Result<Box<dyn std::any::Any + Send>>>,
79    ) -> Result<u64> {
80        let capacity = self.slots.len();
81        for offset in 0..capacity {
82            let slot = (start_slot + offset) % capacity;
83            if self.slots[slot].is_none() {
84                let id = self.generations[slot]
85                    .saturating_mul(capacity as u64)
86                    .saturating_add(slot as u64)
87                    .saturating_add(1);
88                self.slots[slot] = Some(InflightEntry {
89                    id,
90                    op_name,
91                    mapper,
92                    responder,
93                });
94                return Ok(id);
95            }
96        }
97        Err(Error::submission(
98            "ring submission failed: no inflight slot available",
99            "drain completions before submitting more work",
100        ))
101    }
102
103    fn remove(&mut self, id: u64) -> Option<InflightEntry> {
104        let slot = inflight_slot(id, self.slots.len());
105        let matches = self
106            .slots
107            .get(slot)
108            .and_then(Option::as_ref)
109            .is_some_and(|entry| entry.id == id);
110        if matches {
111            self.generations[slot] = self.generations[slot].saturating_add(1);
112            self.slots[slot].take()
113        } else {
114            None
115        }
116    }
117}
118
119#[allow(clippy::cast_possible_truncation)]
120fn inflight_slot(id: u64, capacity: usize) -> usize {
121    // id starts at 1 (0 is reserved/invalid). Saturating sub prevents underflow
122    // if id is ever erroneously 0  -  maps to slot 0 instead of wrapping to MAX.
123    (id.saturating_sub(1) % capacity as u64) as usize
124}
125
126/// Shared ring state used by request handles.
127pub struct RingInner {
128    pub(crate) config: RingConfig,
129    completion_rx: Mutex<Receiver<BackendCompletion>>,
130    inflight: Mutex<InflightTable>,
131    inflight_count: AtomicUsize,
132    backend: AnyBackend,
133    next_slot: AtomicUsize,
134    pid: u32,
135    closed: AtomicBool,
136}
137
138impl RingInner {
139    pub(crate) fn pump_completion(&self, timeout: Option<Duration>) -> Result<CompletionEvent> {
140        self.check_fork()?;
141        let completion = {
142            let receiver = self.completion_rx.lock().map_err(|_| {
143                Error::completion(
144                    "completion receiver mutex was poisoned",
145                    "avoid panicking while waiting for ring completions",
146                )
147            })?;
148            if let Some(timeout) = timeout {
149                match receiver.recv_timeout(timeout) {
150                    Ok(value) => value,
151                    Err(mpsc::RecvTimeoutError::Timeout) => {
152                        return Err(Error::timeout(
153                            "completion wait timed out",
154                            "increase the wait timeout or submit less work concurrently",
155                        ));
156                    }
157                    Err(mpsc::RecvTimeoutError::Disconnected) => {
158                        return Err(Error::completion(
159                            "backend completion channel disconnected",
160                            "keep the ring alive until the backend thread is done",
161                        ));
162                    }
163                }
164            } else {
165                receiver.recv().map_err(|_| {
166                    Error::completion(
167                        "backend completion channel disconnected",
168                        "keep the ring alive until the backend thread is done",
169                    )
170                })?
171            }
172        };
173        self.route_completion(completion)
174    }
175
176    fn route_completion(&self, completion: BackendCompletion) -> Result<CompletionEvent> {
177        let entry = {
178            let mut table = self.inflight.lock().map_err(|_| {
179                Error::completion(
180                    "inflight table mutex was poisoned",
181                    "avoid panicking while routing operation completions",
182                )
183            })?;
184            let entry = table.remove(completion.id).ok_or_else(|| {
185                Error::completion(
186                    "completion for unknown request id",
187                    "avoid completing the same request twice and keep request ids unique",
188                )
189            })?;
190            self.inflight_count.fetch_sub(1, Ordering::Release);
191            entry
192        };
193        let kind = match &completion.result {
194            Ok(_) => CompletionKind::Completed,
195            Err(Error::Canceled { .. }) => CompletionKind::Canceled,
196            Err(_) => CompletionKind::Failed,
197        };
198        let result = completion.result.and_then(entry.mapper);
199        let send_result = entry.responder.send(result).map_err(|_| {
200            Error::completion(
201                "request receiver dropped before completion",
202                "keep the request handle alive until the result is consumed",
203            )
204        });
205        if let Err(error) = send_result {
206            tracing::warn!("{error}");
207        }
208        Ok(CompletionEvent::new(completion.id, entry.op_name, kind))
209    }
210
211    fn check_fork(&self) -> Result<()> {
212        let pid = std::process::id();
213        if pid != self.pid {
214            return Err(Error::ProcessState {
215                message: Cow::Owned(format!(
216                    "ring was created in process {} but used from forked process {}",
217                    self.pid, pid
218                )),
219                fix: Cow::Borrowed(
220                    "create a new ring after fork; io_uring and fallback worker state are not fork-safe",
221                ),
222            });
223        }
224        Ok(())
225    }
226}
227
228impl Drop for RingInner {
229    fn drop(&mut self) {
230        if !self.closed.swap(true, Ordering::AcqRel) {
231            if let Err(error) = self.backend.shutdown() {
232                tracing::warn!("{error}");
233            }
234        }
235    }
236}
237
238/// Submission and completion router.
239#[derive(Clone)]
240pub struct Ring<F: BackendFactory> {
241    inner: Arc<RingInner>,
242    factory: PhantomData<F>,
243}
244
245/// Sequential chain execution context for dependent operations.
246pub struct ChainContext<'a, F: BackendFactory> {
247    ring: &'a Ring<F>,
248}
249
250impl<F: BackendFactory> ChainContext<'_, F> {
251    /// Submits one operation and waits for its typed result.
252    pub fn run<O: Op>(&self, op: O) -> Result<O::Output> {
253        self.ring
254            .submit(op)?
255            .wait(Some(std::time::Duration::from_secs(30)))
256    }
257
258    /// Runs a bounded batch within the surrounding chain.
259    pub fn batch<I, O>(&self, ops: I) -> Result<Vec<O::Output>>
260    where
261        I: IntoIterator<Item = O>,
262        O: Op,
263    {
264        self.ring.batch(ops)
265    }
266}
267
268impl<F: BackendFactory> Ring<F> {
269    /// Creates a ring using the selected backend policy.
270    pub fn new(config: RingConfig) -> Result<Self> {
271        config.validate()?;
272        let (completion_tx, completion_rx) = mpsc::channel();
273        let backend = F::default().create_enum(&config, completion_tx)?;
274        Ok(Self {
275            inner: Arc::new(RingInner {
276                completion_rx: Mutex::new(completion_rx),
277                inflight: Mutex::new(InflightTable::new(config.queue_depth as usize)),
278                config,
279                inflight_count: AtomicUsize::new(0),
280                backend,
281                next_slot: AtomicUsize::new(0),
282                pid: std::process::id(),
283                closed: AtomicBool::new(false),
284            }),
285            factory: PhantomData,
286        })
287    }
288
289    /// Returns the active backend kind.
290    #[must_use]
291    pub fn backend_kind(&self) -> BackendKind {
292        self.inner.backend.kind()
293    }
294
295    /// Returns the configured queue depth.
296    #[must_use]
297    pub fn queue_depth(&self) -> u32 {
298        self.inner.config.queue_depth
299    }
300
301    /// Submits a batch of operations and returns ordered results.
302    ///
303    /// The ring keeps at most `queue_depth` requests in flight at once, so
304    /// callers can batch arbitrarily large iterators without manually chunking.
305    pub fn batch<I, O>(&self, ops: I) -> Result<Vec<O::Output>>
306    where
307        I: IntoIterator<Item = O>,
308        O: Op,
309    {
310        let mut results = Vec::new();
311        let mut inflight: VecDeque<Request<O::Output>> = VecDeque::new();
312        let max_inflight = self.inner.config.queue_depth as usize;
313
314        for op in ops {
315            if inflight.len() == max_inflight {
316                let request = inflight.pop_front().ok_or_else(|| {
317                    Error::completion(
318                        "batch bookkeeping lost an in-flight request",
319                        "keep the batch queue intact while draining completed operations",
320                    )
321                })?;
322                results.push(request.wait(Some(std::time::Duration::from_secs(2)))?);
323            }
324            inflight.push_back(self.submit(op)?);
325        }
326
327        while let Some(request) = inflight.pop_front() {
328            results.push(request.wait(Some(std::time::Duration::from_secs(2)))?);
329        }
330
331        Ok(results)
332    }
333
334    /// Runs a dependent chain of operations in one call.
335    ///
336    /// This is the high-level API for flows where later operations require
337    /// outputs from earlier ones, such as `connect -> send -> recv`.
338    pub fn chain<T>(&self, run: impl FnOnce(ChainContext<'_, F>) -> Result<T>) -> Result<T> {
339        run(ChainContext { ring: self })
340    }
341
342    /// Submits an operation and returns a typed request handle.
343    pub fn submit<O: Op>(&self, op: O) -> Result<Request<O::Output>> {
344        self.inner.check_fork()?;
345        let op_name = op.name();
346        let descriptor = op.into_descriptor()?;
347        let (sender, receiver) = mpsc::channel();
348        let id = {
349            let mut table = self.inner.inflight.lock().map_err(|_| {
350                Error::submission(
351                    "inflight table mutex was poisoned",
352                    "avoid panicking while submitting operations",
353                )
354            })?;
355            let id = table.insert(
356                self.inner.next_slot.fetch_add(1, Ordering::Relaxed)
357                    % self.inner.config.queue_depth as usize,
358                op_name,
359                boxed_mapper::<O>(),
360                sender,
361                )
362                .map_err(|_| {
363                    Error::submission(
364                        format!(
365                            "ring submission failed: SQ full (capacity: {}, in-flight: {})",
366                            self.inner.config.queue_depth,
367                            self.inner.inflight_count.load(Ordering::Acquire)
368                        ),
369                        "increase submission_queue_depth in RingConfig or drain completions before submitting more",
370                    )
371                })?;
372            self.inner.inflight_count.fetch_add(1, Ordering::Release);
373            id
374        };
375        let submission =
376            BackendSubmission::new(id, descriptor).with_timeout(self.inner.config.default_timeout);
377        if let Err(error) = self.inner.backend.submit(submission) {
378            let mut table = self
379                .inner
380                .inflight
381                .lock()
382                .unwrap_or_else(std::sync::PoisonError::into_inner);
383            table.remove(id);
384            self.inner.inflight_count.fetch_sub(1, Ordering::Release);
385            return Err(error);
386        }
387        Ok(Request {
388            id,
389            ring: self.inner.clone(),
390            receiver,
391            marker: std::marker::PhantomData,
392        })
393    }
394
395    /// Blocks until one completion is routed.
396    pub fn complete(&self, timeout: Option<Duration>) -> Result<CompletionEvent> {
397        self.inner.pump_completion(timeout)
398    }
399
400    /// Attempts to cancel a submitted operation.
401    pub fn cancel(&self, id: u64) -> Result<()> {
402        self.inner.check_fork()?;
403        self.inner.backend.cancel(CancellationHandle { target: id })
404    }
405
406    /// Returns the number of tracked in-flight operations.
407    pub fn in_flight(&self) -> Result<usize> {
408        Ok(self.inner.inflight_count.load(Ordering::Acquire))
409    }
410}
411
412impl<F: BackendFactory> Drop for Ring<F> {
413    fn drop(&mut self) {
414        if Arc::strong_count(&self.inner) == 1 && !self.inner.closed.swap(true, Ordering::Relaxed) {
415            if let Err(error) = self.inner.backend.shutdown() {
416                tracing::error!(%error, "wireshift backend shutdown failed during Ring drop");
417            }
418        }
419    }
420}