Skip to main content

laminar_core/io_uring/
config.rs

1//! Configuration types for `io_uring`.
2
3/// Ring operation mode.
4///
5/// Defaults to [`SqPoll`](RingMode::SqPoll) for thread-per-core workloads
6/// (zero-syscall submission). `SqPoll` requires `CAP_SYS_NICE` or root;
7/// ring creation will fail with `EPERM` on unprivileged processes — callers
8/// should fall back to [`Standard`](RingMode::Standard).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum RingMode {
11    /// Standard mode with interrupt-based completions.
12    Standard,
13    /// SQPOLL mode with kernel polling thread (no syscalls under load).
14    /// Requires `CAP_SYS_NICE` or root privileges.
15    #[default]
16    SqPoll,
17    /// IOPOLL mode for `NVMe` devices (polls completions from device).
18    /// Cannot be used with socket operations.
19    IoPoll,
20    /// Combined SQPOLL + IOPOLL for maximum performance on `NVMe`.
21    SqPollIoPoll,
22}
23
24impl RingMode {
25    /// Returns true if SQPOLL is enabled.
26    #[must_use]
27    pub const fn uses_sqpoll(&self) -> bool {
28        matches!(self, Self::SqPoll | Self::SqPollIoPoll)
29    }
30
31    /// Returns true if IOPOLL is enabled.
32    #[must_use]
33    pub const fn uses_iopoll(&self) -> bool {
34        matches!(self, Self::IoPoll | Self::SqPollIoPoll)
35    }
36}
37
38/// Configuration for `io_uring` operations.
39#[derive(Debug, Clone)]
40pub struct IoUringConfig {
41    /// Number of submission queue entries (power of 2, typically 256-4096).
42    pub ring_entries: u32,
43    /// Ring operation mode.
44    pub mode: RingMode,
45    /// SQPOLL idle timeout in milliseconds before kernel thread sleeps.
46    /// Only used when mode uses SQPOLL.
47    pub sqpoll_idle_ms: u32,
48    /// CPU to pin the SQPOLL kernel thread to (usually same NUMA node as core).
49    /// Only used when mode uses SQPOLL.
50    pub sqpoll_cpu: Option<u32>,
51    /// Size of each registered buffer in bytes (typically 64KB).
52    pub buffer_size: usize,
53    /// Number of buffers in the registered pool.
54    pub buffer_count: usize,
55    /// Enable cooperative task running to reduce kernel-userspace transitions.
56    pub coop_taskrun: bool,
57    /// Optimize for single-threaded submission (thread-per-core model).
58    pub single_issuer: bool,
59    /// Enable direct file descriptor table for faster operations.
60    pub direct_table: bool,
61    /// Number of direct file descriptor slots.
62    pub direct_table_size: u32,
63}
64
65impl Default for IoUringConfig {
66    fn default() -> Self {
67        Self {
68            ring_entries: 256,
69            mode: RingMode::default(),
70            sqpoll_idle_ms: 1000,
71            sqpoll_cpu: None,
72            buffer_size: 64 * 1024, // 64KB per buffer
73            buffer_count: 256,      // 256 buffers = 16MB total
74            coop_taskrun: true,
75            single_issuer: true,
76            direct_table: false,
77            direct_table_size: 256,
78        }
79    }
80}
81
82impl IoUringConfig {
83    /// Create a new builder for `IoUringConfig`.
84    #[must_use]
85    pub fn builder() -> IoUringConfigBuilder {
86        IoUringConfigBuilder::default()
87    }
88
89    /// Create configuration with automatic detection.
90    ///
91    /// Detects system capabilities and generates an optimal configuration:
92    /// - Enables SQPOLL mode on Linux 5.11+ with the io-uring feature
93    /// - Enables IOPOLL mode on Linux 5.19+ with `NVMe` storage
94    /// - Uses optimal buffer sizes based on available memory
95    ///
96    /// # Example
97    ///
98    /// ```rust,ignore
99    /// use laminar_core::io_uring::IoUringConfig;
100    ///
101    /// let config = IoUringConfig::auto();
102    /// println!("SQPOLL: {}", config.mode.uses_sqpoll());
103    /// ```
104    #[must_use]
105    pub fn auto() -> Self {
106        let caps = crate::detect::SystemCapabilities::detect();
107
108        let mode = if caps.io_uring.iopoll_supported && caps.storage.device_type.supports_iopoll() {
109            if caps.io_uring.sqpoll_supported {
110                RingMode::SqPollIoPoll
111            } else {
112                RingMode::IoPoll
113            }
114        } else if caps.io_uring.sqpoll_supported {
115            RingMode::SqPoll
116        } else {
117            RingMode::Standard
118        };
119
120        Self {
121            ring_entries: 256,
122            mode,
123            sqpoll_idle_ms: 1000,
124            sqpoll_cpu: None,
125            buffer_size: 64 * 1024,
126            buffer_count: 256,
127            coop_taskrun: caps.io_uring.coop_taskrun,
128            single_issuer: caps.io_uring.single_issuer,
129            direct_table: false,
130            direct_table_size: 256,
131        }
132    }
133
134    /// Total buffer pool size in bytes.
135    #[must_use]
136    pub const fn total_buffer_size(&self) -> usize {
137        self.buffer_size * self.buffer_count
138    }
139
140    /// Validate the configuration.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the configuration is invalid.
145    pub fn validate(&self) -> Result<(), super::IoUringError> {
146        // Ring entries must be power of 2
147        if !self.ring_entries.is_power_of_two() {
148            return Err(super::IoUringError::InvalidConfig(format!(
149                "ring_entries must be power of 2, got {}",
150                self.ring_entries
151            )));
152        }
153
154        // Must have at least 2 entries
155        if self.ring_entries < 2 {
156            return Err(super::IoUringError::InvalidConfig(
157                "ring_entries must be at least 2".to_string(),
158            ));
159        }
160
161        // Buffer size must be positive and reasonable
162        if self.buffer_size == 0 {
163            return Err(super::IoUringError::InvalidConfig(
164                "buffer_size must be positive".to_string(),
165            ));
166        }
167
168        if self.buffer_size > 16 * 1024 * 1024 {
169            return Err(super::IoUringError::InvalidConfig(
170                "buffer_size cannot exceed 16MB".to_string(),
171            ));
172        }
173
174        // Buffer count must be reasonable
175        if self.buffer_count == 0 {
176            return Err(super::IoUringError::InvalidConfig(
177                "buffer_count must be positive".to_string(),
178            ));
179        }
180
181        if self.buffer_count > 65536 {
182            return Err(super::IoUringError::InvalidConfig(
183                "buffer_count cannot exceed 65536".to_string(),
184            ));
185        }
186
187        Ok(())
188    }
189}
190
191/// Builder for `IoUringConfig`.
192#[derive(Debug, Default)]
193pub struct IoUringConfigBuilder {
194    config: IoUringConfig,
195}
196
197impl IoUringConfigBuilder {
198    /// Set the number of ring entries.
199    #[must_use]
200    pub const fn ring_entries(mut self, entries: u32) -> Self {
201        self.config.ring_entries = entries;
202        self
203    }
204
205    /// Set the ring mode.
206    #[must_use]
207    pub const fn mode(mut self, mode: RingMode) -> Self {
208        self.config.mode = mode;
209        self
210    }
211
212    /// Enable SQPOLL mode with the specified idle timeout.
213    #[must_use]
214    pub const fn enable_sqpoll(mut self, idle_ms: u32) -> Self {
215        self.config.mode = RingMode::SqPoll;
216        self.config.sqpoll_idle_ms = idle_ms;
217        self
218    }
219
220    /// Set the CPU for the SQPOLL kernel thread.
221    #[must_use]
222    pub const fn sqpoll_cpu(mut self, cpu: u32) -> Self {
223        self.config.sqpoll_cpu = Some(cpu);
224        self
225    }
226
227    /// Enable IOPOLL mode (for `NVMe` devices).
228    #[must_use]
229    pub const fn enable_iopoll(mut self) -> Self {
230        self.config.mode = match self.config.mode {
231            RingMode::SqPoll | RingMode::SqPollIoPoll => RingMode::SqPollIoPoll,
232            _ => RingMode::IoPoll,
233        };
234        self
235    }
236
237    /// Set the size of each buffer in the pool.
238    #[must_use]
239    pub const fn buffer_size(mut self, size: usize) -> Self {
240        self.config.buffer_size = size;
241        self
242    }
243
244    /// Set the number of buffers in the pool.
245    #[must_use]
246    pub const fn buffer_count(mut self, count: usize) -> Self {
247        self.config.buffer_count = count;
248        self
249    }
250
251    /// Enable cooperative task running.
252    #[must_use]
253    pub const fn coop_taskrun(mut self, enable: bool) -> Self {
254        self.config.coop_taskrun = enable;
255        self
256    }
257
258    /// Enable single-issuer optimization.
259    #[must_use]
260    pub const fn single_issuer(mut self, enable: bool) -> Self {
261        self.config.single_issuer = enable;
262        self
263    }
264
265    /// Enable direct file descriptor table.
266    #[must_use]
267    pub const fn direct_table(mut self, size: u32) -> Self {
268        self.config.direct_table = true;
269        self.config.direct_table_size = size;
270        self
271    }
272
273    /// Build the configuration.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if the configuration is invalid.
278    pub fn build(self) -> Result<IoUringConfig, super::IoUringError> {
279        self.config.validate()?;
280        Ok(self.config)
281    }
282
283    /// Build the configuration without validation (for testing).
284    #[must_use]
285    pub fn build_unchecked(self) -> IoUringConfig {
286        self.config
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_default_config() {
296        let config = IoUringConfig::default();
297        assert_eq!(config.ring_entries, 256);
298        assert_eq!(config.mode, RingMode::SqPoll);
299        assert_eq!(config.buffer_size, 64 * 1024);
300        assert_eq!(config.buffer_count, 256);
301        assert!(config.coop_taskrun);
302        assert!(config.single_issuer);
303    }
304
305    #[test]
306    fn test_builder() {
307        let config = IoUringConfig::builder()
308            .ring_entries(512)
309            .enable_sqpoll(2000)
310            .sqpoll_cpu(4)
311            .buffer_size(128 * 1024)
312            .buffer_count(512)
313            .build()
314            .unwrap();
315
316        assert_eq!(config.ring_entries, 512);
317        assert!(config.mode.uses_sqpoll());
318        assert_eq!(config.sqpoll_idle_ms, 2000);
319        assert_eq!(config.sqpoll_cpu, Some(4));
320        assert_eq!(config.buffer_size, 128 * 1024);
321        assert_eq!(config.buffer_count, 512);
322    }
323
324    #[test]
325    fn test_ring_mode() {
326        assert!(!RingMode::Standard.uses_sqpoll());
327        assert!(!RingMode::Standard.uses_iopoll());
328
329        assert!(RingMode::SqPoll.uses_sqpoll());
330        assert!(!RingMode::SqPoll.uses_iopoll());
331
332        assert!(!RingMode::IoPoll.uses_sqpoll());
333        assert!(RingMode::IoPoll.uses_iopoll());
334
335        assert!(RingMode::SqPollIoPoll.uses_sqpoll());
336        assert!(RingMode::SqPollIoPoll.uses_iopoll());
337    }
338
339    #[test]
340    fn test_total_buffer_size() {
341        let config = IoUringConfig {
342            buffer_size: 64 * 1024,
343            buffer_count: 256,
344            ..Default::default()
345        };
346        assert_eq!(config.total_buffer_size(), 16 * 1024 * 1024); // 16MB
347    }
348
349    #[test]
350    fn test_validation_ring_entries_power_of_two() {
351        let config = IoUringConfig {
352            ring_entries: 100, // Not power of 2
353            ..Default::default()
354        };
355        assert!(config.validate().is_err());
356    }
357
358    #[test]
359    fn test_validation_buffer_size_zero() {
360        let config = IoUringConfig {
361            buffer_size: 0,
362            ..Default::default()
363        };
364        assert!(config.validate().is_err());
365    }
366
367    #[test]
368    fn test_validation_buffer_count_zero() {
369        let config = IoUringConfig {
370            buffer_count: 0,
371            ..Default::default()
372        };
373        assert!(config.validate().is_err());
374    }
375
376    #[test]
377    fn test_enable_iopoll_combines_with_sqpoll() {
378        let config = IoUringConfig::builder()
379            .enable_sqpoll(1000)
380            .enable_iopoll()
381            .build_unchecked();
382
383        assert_eq!(config.mode, RingMode::SqPollIoPoll);
384    }
385
386    #[test]
387    fn test_io_uring_config_auto() {
388        let config = IoUringConfig::auto();
389
390        // Auto config should have valid default values
391        assert_eq!(config.ring_entries, 256);
392        assert_eq!(config.buffer_size, 64 * 1024);
393        assert_eq!(config.buffer_count, 256);
394
395        // Validation should pass
396        assert!(config.validate().is_ok());
397
398        // Mode depends on platform capabilities
399        // On non-Linux or without io-uring feature, should be Standard
400        #[cfg(not(all(target_os = "linux", feature = "io-uring")))]
401        {
402            assert_eq!(config.mode, RingMode::Standard);
403        }
404    }
405}