tpt_torus_core/lib.rs
1//! TPT Torus Core — the Virtual Torus abstraction.
2//!
3//! Exposes the [`Torus`] handle, and the [`Flow`] (submission) / [`Result`] (completion)
4//! types that replace raw SQE/CQE across all backends.
5
6pub mod async_api;
7#[cfg(feature = "tokio")]
8pub mod async_tokio;
9pub mod backend;
10pub mod cgroup;
11pub mod error;
12pub mod flow;
13pub mod lease;
14pub mod observability;
15pub mod operation;
16pub mod raw_api;
17pub mod result;
18pub mod rings;
19pub mod torus_panic;
20
21pub use error::{Error, Result};
22pub use flow::Flow;
23pub use lease::{LeaseError, LeaseRegistry, SharedLeaseRegistry};
24pub use operation::{IoSlice, Operation};
25pub use result::Result as TorusResult;
26pub use rings::{CompletionRing, SubmissionRing};
27pub use torus_panic::TorusPanic;
28
29use backend::Backend;
30use std::sync::atomic::{AtomicU32, Ordering};
31use std::sync::{Arc, Mutex};
32
33/// The main context object for the Virtual Torus.
34///
35/// `Torus` owns the virtual submission and completion rings and delegates
36/// to a platform-specific [`Backend`] for actual I/O. It is thread-safe
37/// and can be shared across threads via `Arc<Torus>`.
38pub struct Torus {
39 sq: SubmissionRing,
40 cq: CompletionRing,
41 backend: Mutex<Box<dyn Backend>>,
42 /// In-flight operation spans, keyed by `user_data`, consumed on `reap`.
43 /// Only present when the `tracing` feature is enabled.
44 #[cfg(feature = "tracing")]
45 spans: Mutex<std::collections::HashMap<u64, crate::observability::FlowSpan>>,
46}
47
48// SAFETY: Torus is thread-safe. The backend is behind a Mutex, and the rings
49// use atomic operations for synchronization.
50unsafe impl Send for Torus {}
51unsafe impl Sync for Torus {}
52
53impl Torus {
54 /// Create a new Torus instance with the given ring size and backend.
55 ///
56 /// `ring_entries` must be a power of two (e.g. 256, 1024, 4096).
57 pub fn new(ring_entries: u32, backend: Box<dyn Backend>) -> Result<Self> {
58 if !ring_entries.is_power_of_two() {
59 return Err(Error::InvalidParam("ring_entries must be a power of two"));
60 }
61 Ok(Self {
62 sq: SubmissionRing::new(ring_entries),
63 cq: CompletionRing::new(ring_entries),
64 backend: Mutex::new(backend),
65 #[cfg(feature = "tracing")]
66 spans: Mutex::new(std::collections::HashMap::new()),
67 })
68 }
69
70 /// Submit a single flow to the Virtual Torus.
71 pub fn submit(&self, flow: &Flow) -> Result<()> {
72 let n = self
73 .backend
74 .lock()
75 .unwrap()
76 .submit(std::slice::from_ref(flow))?;
77 if n == 0 {
78 return Err(Error::SubmissionFull);
79 }
80 #[cfg(feature = "tracing")]
81 {
82 self.spans
83 .lock()
84 .unwrap()
85 .insert(flow.user_data(), crate::observability::FlowSpan::new(flow));
86 }
87 Ok(())
88 }
89
90 /// Submit a batch of flows to the Virtual Torus.
91 pub fn submit_batch(&self, flows: &[Flow]) -> Result<usize> {
92 let n = self.backend.lock().unwrap().submit(flows)?;
93 #[cfg(feature = "tracing")]
94 {
95 let mut spans = self.spans.lock().unwrap();
96 for flow in flows.iter().take(n) {
97 spans.insert(flow.user_data(), crate::observability::FlowSpan::new(flow));
98 }
99 }
100 Ok(n)
101 }
102
103 /// Submit a vectored read (readv) operation.
104 ///
105 /// Reads from `fd` at `offset` into multiple buffers described by `bufs`.
106 /// Returns the total number of bytes read across all buffers.
107 pub fn readv(&self, fd: i32, bufs: &[IoSlice], offset: u64, user_data: u64) -> Result<()> {
108 let flow = Flow::with_user_data(
109 Operation::Readv {
110 fd,
111 bufs: bufs.as_ptr(),
112 buf_count: bufs.len() as u32,
113 offset,
114 },
115 user_data,
116 );
117 self.submit(&flow)
118 }
119
120 /// Submit a vectored write (writev) operation.
121 ///
122 /// Writes to `fd` at `offset` from multiple buffers described by `bufs`.
123 /// Returns the total number of bytes written across all buffers.
124 pub fn writev(&self, fd: i32, bufs: &[IoSlice], offset: u64, user_data: u64) -> Result<()> {
125 let flow = Flow::with_user_data(
126 Operation::Writev {
127 fd,
128 bufs: bufs.as_ptr(),
129 buf_count: bufs.len() as u32,
130 offset,
131 },
132 user_data,
133 );
134 self.submit(&flow)
135 }
136
137 /// Reap all available completions.
138 pub fn reap(&self, results: &mut Vec<TorusResult>) -> Result<usize> {
139 let count = self.backend.lock().unwrap().reap(results)?;
140 #[cfg(feature = "tracing")]
141 {
142 let mut spans = self.spans.lock().unwrap();
143 for r in results.iter() {
144 if let Some(span) = spans.remove(&r.user_data) {
145 span.complete(r.result);
146 }
147 }
148 }
149 Ok(count)
150 }
151
152 /// Block until at least one completion is available.
153 pub fn wait(&self, timeout_us: u64) -> Result<()> {
154 #[cfg(feature = "tracing")]
155 let _span = tracing::debug_span!("torus_wait", timeout_us = timeout_us).entered();
156 self.backend.lock().unwrap().wait(timeout_us)
157 }
158
159 /// Number of in-flight operations.
160 pub fn in_flight(&self) -> u32 {
161 self.backend.lock().unwrap().in_flight()
162 }
163
164 /// Access the virtual submission ring.
165 pub fn submission_ring(&self) -> &SubmissionRing {
166 &self.sq
167 }
168
169 /// Access the virtual completion ring.
170 pub fn completion_ring(&self) -> &CompletionRing {
171 &self.cq
172 }
173
174 /// Register all buffers currently tracked by `registry` with the OS kernel
175 /// for zero-copy fixed-buffer I/O (io_uring `IORING_REGISTER_BUFFERS`).
176 ///
177 /// After this call, `read`/`write` operations whose buffer matches a
178 /// registered region base will be issued as `IORING_OP_READ_FIXED` /
179 /// `WRITE_FIXED`, skipping per-operation address translation in the kernel.
180 ///
181 /// # Example
182 ///
183 /// ```no_run
184 /// use tpt_torus_core::lease::LeaseRegistry;
185 /// use tpt_torus_core::Torus;
186 /// # fn make_torus() -> Torus { unimplemented!() }
187 /// let torus = make_torus();
188 /// let registry = LeaseRegistry::new();
189 /// let mut buf = vec![0u8; 4096];
190 /// unsafe { registry.register(buf.as_mut_ptr(), buf.len()) };
191 /// torus.register_leases(®istry)?; // enables IORING_OP_READ/WRITE_FIXED
192 /// # Ok::<(), tpt_torus_core::Error>(())
193 /// ```
194 ///
195 /// # Platform notes
196 /// - Linux (io_uring): registers the regions with the kernel immediately.
197 /// - Other platforms: this is a no-op (no fixed-buffer mechanism available).
198 #[cfg(unix)]
199 pub fn register_leases(&self, registry: &LeaseRegistry) -> crate::error::Result<()> {
200 let buffers = registry.as_register_buffers();
201 if buffers.is_empty() {
202 return Ok(());
203 }
204 self.backend.lock().unwrap().register_buffers(&buffers)
205 }
206
207 /// Register lease buffers with the kernel.
208 ///
209 /// No-op on platforms without a fixed-buffer mechanism. See the Unix
210 /// implementation of [`Torus::register_leases`].
211 #[cfg(not(unix))]
212 pub fn register_leases(&self, _registry: &LeaseRegistry) -> crate::error::Result<()> {
213 Ok(())
214 }
215
216 /// Get raw, unguarded access to the Torus, bypassing Buffer Leasing.
217 ///
218 /// # Safety
219 ///
220 /// The returned [`RawTorus`] bypasses all buffer safety checks.
221 /// The caller is responsible for ensuring buffer validity.
222 pub unsafe fn raw(&self) -> raw_api::RawTorus<'_> {
223 raw_api::RawTorus::new(self)
224 }
225}
226
227/// Shared handle to a `Torus` instance, suitable for multi-threaded use.
228pub type SharedTorus = Arc<Torus>;
229
230/// A pool of `Torus` instances that distributes I/O across multiple backends.
231///
232/// `TorusPool` avoids serializing all I/O through a single `Mutex<dyn Backend>`
233/// by maintaining N independent `Torus` instances and distributing operations
234/// across them via round-robin. Each `Torus` in the pool has its own backend
235/// and ring pair, so submissions on different pool entries are fully concurrent.
236///
237/// # Example
238///
239/// ```ignore
240/// use tpt_torus_core::TorusPool;
241///
242/// // Create a pool with 4 Torus instances (one per core)
243/// let pool = TorusPool::new(4, 256, |ring_entries| {
244/// Box::new(UringBackend::new(ring_entries)?)
245/// })?;
246///
247/// // Submit operations — distributed across pool members
248/// pool.submit(&flow)?;
249/// ```
250pub struct TorusPool {
251 instances: Vec<Arc<Torus>>,
252 next: AtomicU32,
253}
254
255impl TorusPool {
256 /// Create a new pool with `count` Torus instances.
257 ///
258 /// Each instance gets `ring_entries` SQ/CQ entries. The `make_backend`
259 /// closure is called once per instance to create the platform-specific backend.
260 pub fn new<F>(count: usize, ring_entries: u32, make_backend: F) -> Result<Self>
261 where
262 F: Fn(u32) -> Result<Box<dyn Backend>>,
263 {
264 if count == 0 {
265 return Err(Error::InvalidParam("pool count must be > 0"));
266 }
267 let mut instances = Vec::with_capacity(count);
268 for _ in 0..count {
269 let backend = make_backend(ring_entries)?;
270 instances.push(Arc::new(Torus::new(ring_entries, backend)?));
271 }
272 Ok(Self {
273 instances,
274 next: AtomicU32::new(0),
275 })
276 }
277
278 /// Submit a flow to the next available Torus instance (round-robin).
279 pub fn submit(&self, flow: &Flow) -> Result<()> {
280 let idx = self.next.fetch_add(1, Ordering::Relaxed) as usize % self.instances.len();
281 self.instances[idx].submit(flow)
282 }
283
284 /// Submit a batch of flows, distributing them across pool instances.
285 pub fn submit_batch(&self, flows: &[Flow]) -> Result<usize> {
286 let mut total = 0;
287 for flow in flows {
288 self.submit(flow)?;
289 total += 1;
290 }
291 Ok(total)
292 }
293
294 /// Reap completions from all pool instances.
295 pub fn reap(&self, results: &mut Vec<TorusResult>) -> Result<usize> {
296 let mut total = 0;
297 for instance in &self.instances {
298 total += instance.reap(results)?;
299 }
300 Ok(total)
301 }
302
303 /// The number of Torus instances in this pool.
304 pub fn len(&self) -> usize {
305 self.instances.len()
306 }
307
308 /// Whether the pool is empty.
309 pub fn is_empty(&self) -> bool {
310 self.instances.is_empty()
311 }
312
313 /// Get a reference to a specific pool instance.
314 pub fn get(&self, index: usize) -> Option<&Torus> {
315 self.instances.get(index).map(|arc| arc.as_ref())
316 }
317
318 /// Total in-flight operations across all pool instances.
319 pub fn in_flight(&self) -> u32 {
320 self.instances.iter().map(|i| i.in_flight()).sum()
321 }
322}