1use 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.saturating_sub(1) % capacity as u64) as usize
124}
125
126pub 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#[derive(Clone)]
240pub struct Ring<F: BackendFactory> {
241 inner: Arc<RingInner>,
242 factory: PhantomData<F>,
243}
244
245pub struct ChainContext<'a, F: BackendFactory> {
247 ring: &'a Ring<F>,
248}
249
250impl<F: BackendFactory> ChainContext<'_, F> {
251 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 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 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 #[must_use]
291 pub fn backend_kind(&self) -> BackendKind {
292 self.inner.backend.kind()
293 }
294
295 #[must_use]
297 pub fn queue_depth(&self) -> u32 {
298 self.inner.config.queue_depth
299 }
300
301 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 pub fn chain<T>(&self, run: impl FnOnce(ChainContext<'_, F>) -> Result<T>) -> Result<T> {
339 run(ChainContext { ring: self })
340 }
341
342 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 pub fn complete(&self, timeout: Option<Duration>) -> Result<CompletionEvent> {
397 self.inner.pump_completion(timeout)
398 }
399
400 pub fn cancel(&self, id: u64) -> Result<()> {
402 self.inner.check_fork()?;
403 self.inner.backend.cancel(CancellationHandle { target: id })
404 }
405
406 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}