Skip to main content

clt_database/io/
io_uring.rs

1#![allow(clippy::arc_with_non_send_sync)]
2
3use super::{
4    common, Completion, CompletionInner, File, OpenFlags, SharedWalLockKind, SharedWalMappedRegion,
5    IO,
6};
7use crate::error::io_error;
8use crate::io::clock::{Clock, DefaultClock, MonotonicInstant, WallClockInstant};
9use crate::io::unix::{
10    unix_shared_wal_lock_byte, unix_shared_wal_map, unix_shared_wal_unlock_byte,
11};
12use crate::storage::wal::CKPT_BATCH_PAGES;
13use crate::sync::Mutex;
14use crate::turso_assert;
15use crate::{CompletionError, LimboError, Result};
16use rustix::fs::{self, FlockOperation, OFlags};
17use std::ptr::NonNull;
18use std::{
19    collections::{HashMap, VecDeque},
20    io::ErrorKind,
21    ops::Deref,
22    os::{fd::AsFd, unix::io::AsRawFd},
23    sync::Arc,
24};
25use tracing::{debug, trace, warn};
26
27/// Size of the io_uring submission and completion queues
28const ENTRIES: u32 = 512;
29
30/// Idle timeout for the sqpoll kernel thread before it needs
31/// to be woken back up by a call IORING_ENTER_SQ_WAKEUP flag.
32/// (handled by the io_uring crate in `submit_and_wait`)
33const SQPOLL_IDLE: u32 = 1000;
34
35/// Number of Vec<Box<[iovec]>> we preallocate on initialization
36const IOVEC_POOL_SIZE: usize = 64;
37
38/// Maximum number of iovec entries per writev operation.
39/// IOV_MAX is typically 1024
40const MAX_IOVEC_ENTRIES: usize = CKPT_BATCH_PAGES;
41
42/// Maximum number of I/O operations to wait for in a single run,
43/// waiting for > 1 can reduce the amount of `io_uring_enter` syscalls we
44/// make, but can increase single operation latency.
45const MAX_WAIT: usize = 4;
46
47/// One memory arena for DB pages and another for WAL frames
48const ARENA_COUNT: usize = 2;
49
50/// user_data tag for cancellation operations
51const CANCEL_TAG: u64 = 1;
52
53/// Probed io_uring opcode support. Opcodes that are not supported by the
54/// running kernel fall back to synchronous POSIX syscalls.
55struct UringCapabilities {
56    ftruncate: bool,
57}
58
59pub struct UringIO {
60    /// The io_uring instance, shared by ref. `submit_and_wait`, `submit`, and
61    /// `submitter()` only need `&self` — multiple threads can issue these
62    /// concurrently and the kernel handles serialization via the ring's
63    /// atomic head/tail. Pulling the ring out of the Mutex means a thread
64    /// blocked on `submit_and_wait` doesn't block other threads from pushing
65    /// new SQEs or draining the CQ.
66    ring: Arc<io_uring::IoUring>,
67    /// Mutex-guarded auxiliary state. The Mutex is held only briefly:
68    /// during `submission_shared()`-backed pushes, and during
69    /// `completion_shared()`-backed CQ drains. The unsafe `_shared()` APIs
70    /// require that no other SQ/CQ object exists; holding this Mutex
71    /// satisfies that invariant.
72    state: Arc<Mutex<RingState>>,
73    /// Serializes the blocking `submit_and_wait` syscall. Multiple
74    /// concurrent waiters are unsafe: when N threads each ask the kernel
75    /// for `min_complete=K` events and only K total arrive, the kernel
76    /// satisfies *one* waiter (Linux wakes a single waiter from the
77    /// `io_sq_data` wait queue) — the others stay blocked on completions
78    /// that won't arrive. We avoid that by ensuring at most one thread is
79    /// inside `submit_and_wait` at any time. Submission and CQ drain run
80    /// under `state` and aren't blocked by this lock.
81    wait_lock: Arc<Mutex<()>>,
82    caps: Arc<UringCapabilities>,
83}
84
85unsafe impl Send for UringIO {}
86unsafe impl Sync for UringIO {}
87crate::assert::assert_send_sync!(UringIO);
88
89struct RingState {
90    pending_ops: usize,
91    writev_states: HashMap<u64, WritevState>,
92    overflow: VecDeque<io_uring::squeue::Entry>,
93    iov_pool: IovecPool,
94    free_arenas: [Option<(NonNull<u8>, usize)>; ARENA_COUNT],
95}
96
97impl RingState {
98    fn empty(&self) -> bool {
99        self.pending_ops == 0 && self.overflow.is_empty()
100    }
101
102    /// SAFETY: caller must guarantee no other `SubmissionQueue` exists for
103    /// `ring`. Holding the `RingState` Mutex satisfies that invariant.
104    unsafe fn flush_overflow(&mut self, ring: &io_uring::IoUring) {
105        if self.overflow.is_empty() {
106            return;
107        }
108        let mut sq = ring.submission_shared();
109        while !self.overflow.is_empty() {
110            if sq.is_full() {
111                break;
112            }
113            let entry = self.overflow.pop_front().expect("checked not empty");
114            if sq.push(&entry).is_err() {
115                self.overflow.push_front(entry);
116                break;
117            }
118            self.pending_ops += 1;
119        }
120    }
121
122    /// SAFETY: caller must guarantee no other `SubmissionQueue` exists for
123    /// `ring` (i.e. caller holds the `RingState` Mutex).
124    unsafe fn submit_entry(&mut self, ring: &io_uring::IoUring, entry: &io_uring::squeue::Entry) {
125        trace!("submit_entry({:?})", entry);
126        self.flush_overflow(ring);
127        let pushed = {
128            let mut sq = ring.submission_shared();
129            sq.push(entry).is_ok()
130        };
131        if pushed {
132            self.pending_ops += 1;
133            return;
134        }
135        // SQ full — buffer locally and ask the kernel to drain so the next
136        // attempt has space.
137        self.overflow.push_back(entry.clone());
138        let _ = ring.submit();
139    }
140
141    /// SAFETY: same contract as `submit_entry`.
142    unsafe fn submit_cancel_urgent(
143        &mut self,
144        ring: &io_uring::IoUring,
145        entry: &io_uring::squeue::Entry,
146    ) -> Result<()> {
147        let pushed = {
148            let mut sq = ring.submission_shared();
149            sq.push(entry).is_ok()
150        };
151        if pushed {
152            self.pending_ops += 1;
153            return Ok(());
154        }
155        self.overflow.push_front(entry.clone());
156        ring.submit().map_err(|e| io_error(e, "io_uring_submit"))?;
157        Ok(())
158    }
159}
160
161/// preallocated vec of iovec arrays to avoid allocations during writev operations
162struct IovecPool {
163    pool: Vec<Box<[libc::iovec; MAX_IOVEC_ENTRIES]>>,
164}
165
166impl IovecPool {
167    fn new() -> Self {
168        let pool = (0..IOVEC_POOL_SIZE)
169            .map(|_| {
170                Box::new(
171                    [libc::iovec {
172                        iov_base: std::ptr::null_mut(),
173                        iov_len: 0,
174                    }; MAX_IOVEC_ENTRIES],
175                )
176            })
177            .collect();
178        Self { pool }
179    }
180
181    #[inline(always)]
182    fn acquire(&mut self) -> Option<Box<[libc::iovec; MAX_IOVEC_ENTRIES]>> {
183        self.pool.pop()
184    }
185
186    #[inline(always)]
187    fn release(&mut self, iovec: Box<[libc::iovec; MAX_IOVEC_ENTRIES]>) {
188        if self.pool.len() < IOVEC_POOL_SIZE {
189            self.pool.push(iovec);
190        }
191    }
192}
193
194impl UringIO {
195    pub fn new() -> Result<Self> {
196        let ring = match io_uring::IoUring::builder()
197            .setup_sqpoll(SQPOLL_IDLE)
198            .build(ENTRIES)
199        {
200            Ok(ring) => ring,
201            Err(_) => io_uring::IoUring::new(ENTRIES).map_err(|e| io_error(e, "io_uring_setup"))?,
202        };
203        // RL_MEMLOCK cap is typically 8MB, the current design is to have one large arena
204        // registered at startup and therefore we can simply use the zero index, falling back
205        // to similar logic as the existing buffer pool for cases where it is over capacity.
206        ring.submitter()
207            .register_buffers_sparse(ARENA_COUNT as u32)
208            .map_err(|e| io_error(e, "register_buffers"))?;
209        // Probe supported opcodes so we can fall back to POSIX for unsupported ones.
210        let mut probe = io_uring::register::Probe::new();
211        let caps = if ring.submitter().register_probe(&mut probe).is_ok() {
212            UringCapabilities {
213                ftruncate: probe.is_supported(io_uring::opcode::Ftruncate::CODE),
214            }
215        } else {
216            UringCapabilities { ftruncate: false }
217        };
218        if !caps.ftruncate {
219            warn!("io_uring: IORING_OP_FTRUNCATE not supported by kernel, using POSIX fallback");
220        }
221        let state = RingState {
222            overflow: VecDeque::new(),
223            pending_ops: 0,
224            writev_states: HashMap::default(),
225            iov_pool: IovecPool::new(),
226            free_arenas: [const { None }; ARENA_COUNT],
227        };
228        debug!("Using IO backend 'io-uring'");
229        Ok(Self {
230            ring: Arc::new(ring),
231            state: Arc::new(Mutex::new(state)),
232            wait_lock: Arc::new(Mutex::new(())),
233            caps: Arc::new(caps),
234        })
235    }
236}
237
238/// State to track an ongoing writev operation in
239/// the case of a partial write.
240struct WritevState {
241    /// File descriptor/id of the file we are writing to
242    file_id: io_uring::types::Fd,
243    /// absolute file offset for next submit
244    file_pos: u64,
245    /// current buffer index in `bufs`
246    current_buffer_idx: usize,
247    /// intra-buffer offset
248    current_buffer_offset: usize,
249    /// total bytes written so far
250    total_written: usize,
251    /// cache the sum of all buffer lengths for the total expected write
252    total_len: usize,
253    /// buffers to write
254    bufs: Vec<Arc<crate::Buffer>>,
255    /// we keep the last iovec allocation alive until final CQE
256    last_iov_allocation: Option<Box<[libc::iovec; MAX_IOVEC_ENTRIES]>>,
257}
258
259impl WritevState {
260    fn new(file: &UringFile, pos: u64, bufs: Vec<Arc<crate::Buffer>>) -> Self {
261        let file_id = file.file.as_raw_fd();
262        let total_len = bufs.iter().map(|b| b.len()).sum();
263        Self {
264            file_id: io_uring::types::Fd(file_id),
265            file_pos: pos,
266            current_buffer_idx: 0,
267            current_buffer_offset: 0,
268            total_written: 0,
269            bufs,
270            last_iov_allocation: None,
271            total_len,
272        }
273    }
274
275    #[inline(always)]
276    fn remaining(&self) -> usize {
277        self.total_len - self.total_written
278    }
279
280    /// Advance (idx, off, pos) after written bytes
281    #[inline(always)]
282    fn advance(&mut self, written: u64) {
283        let mut remaining = written;
284        while remaining > 0 {
285            let current_buf_len = self.bufs[self.current_buffer_idx].len();
286            let left = current_buf_len - self.current_buffer_offset;
287            if remaining < left as u64 {
288                self.current_buffer_offset += remaining as usize;
289                self.file_pos += remaining;
290                remaining = 0;
291            } else {
292                remaining -= left as u64;
293                self.file_pos += left as u64;
294                self.current_buffer_idx += 1;
295                self.current_buffer_offset = 0;
296            }
297        }
298        self.total_written += written as usize;
299    }
300
301    #[inline(always)]
302    /// Free the allocation that keeps the iovec array alive while writev is ongoing
303    fn free_last_iov(&mut self, pool: &mut IovecPool) {
304        if let Some(allocation) = self.last_iov_allocation.take() {
305            pool.release(allocation);
306        }
307    }
308}
309
310impl RingState {
311    #[cfg(debug_assertions)]
312    fn debug_check_fixed(&self, idx: u32, ptr: *const u8, len: usize) {
313        let (base, blen) = self.free_arenas[idx as usize].expect("slot not registered");
314        let start = base.as_ptr() as usize;
315        let end = start + blen;
316        let p = ptr as usize;
317        turso_assert!(
318            p >= start && p + len <= end,
319            "Fixed operation, pointer out of registered range"
320        );
321    }
322
323    /// Submit or resubmit a writev operation. SAFETY: caller must hold the
324    /// `RingState` Mutex (so no other `SubmissionQueue` exists for `ring`).
325    unsafe fn submit_writev(&mut self, ring: &io_uring::IoUring, key: u64, mut st: WritevState) {
326        st.free_last_iov(&mut self.iov_pool);
327
328        let mut iov_allocation = self.iov_pool.acquire().unwrap_or_else(|| {
329            Box::new(
330                [libc::iovec {
331                    iov_base: std::ptr::null_mut(),
332                    iov_len: 0,
333                }; MAX_IOVEC_ENTRIES],
334            )
335        });
336
337        let mut iov_count = 0;
338        let mut last_end: Option<(*const u8, usize)> = None;
339
340        for (idx, buffer) in st.bufs.iter().enumerate().skip(st.current_buffer_idx) {
341            let mut ptr = buffer.as_ptr();
342            let mut len = buffer.len();
343            if idx == st.current_buffer_idx && st.current_buffer_offset != 0 {
344                turso_assert!(
345                    st.current_buffer_offset <= len,
346                    "writev state offset out of bounds"
347                );
348                ptr = ptr.add(st.current_buffer_offset);
349                len -= st.current_buffer_offset;
350            }
351            if let Some((last_ptr, last_len)) = last_end {
352                if last_ptr.add(last_len) == ptr {
353                    iov_allocation[iov_count - 1].iov_len += len;
354                    last_end = Some((last_ptr, last_len + len));
355                    continue;
356                }
357            }
358            iov_allocation[iov_count] = libc::iovec {
359                iov_base: ptr as *mut _,
360                iov_len: len,
361            };
362            last_end = Some((ptr, len));
363            iov_count += 1;
364            if iov_count >= MAX_IOVEC_ENTRIES {
365                break;
366            }
367        }
368
369        let ptr = iov_allocation.as_ptr() as *mut libc::iovec;
370        st.last_iov_allocation = Some(iov_allocation);
371        let entry = io_uring::opcode::Writev::new(st.file_id, ptr, iov_count as u32)
372            .offset(st.file_pos)
373            .build()
374            .user_data(key);
375        self.writev_states.insert(key, st);
376        self.submit_entry(ring, &entry);
377    }
378
379    /// Handle a writev CQE. SAFETY: caller must hold the `RingState` Mutex.
380    unsafe fn handle_writev_completion(
381        &mut self,
382        ring: &io_uring::IoUring,
383        mut state: WritevState,
384        user_data: u64,
385        result: i32,
386    ) {
387        if result < 0 {
388            let err = std::io::Error::from_raw_os_error(-result);
389            tracing::error!("writev failed (user_data: {}): {}", user_data, err);
390            state.free_last_iov(&mut self.iov_pool);
391            completion_from_key(user_data).error(CompletionError::IOError(err.kind(), "pwritev"));
392            return;
393        }
394
395        let written = result;
396
397        if written == 0 && state.remaining() > 0 {
398            state.free_last_iov(&mut self.iov_pool);
399            completion_from_key(user_data).error(CompletionError::ShortWrite);
400            return;
401        }
402        state.advance(written as u64);
403
404        match state.remaining() {
405            0 => {
406                tracing::debug!(
407                    "writev operation completed: wrote {} bytes",
408                    state.total_written
409                );
410                state.free_last_iov(&mut self.iov_pool);
411                completion_from_key(user_data).complete(state.total_written as i32);
412            }
413            remaining => {
414                tracing::trace!(
415                    "resubmitting writev operation for user_data {}: wrote {} bytes, remaining {}",
416                    user_data,
417                    written,
418                    remaining
419                );
420                self.submit_writev(ring, user_data, state);
421                // Progress wake: the future is parked on the parent
422                // completion's waker, but `complete()` only fires on the
423                // final chunk. Without this, intermediate-chunk completions
424                // never wake the future, the resubmitted chunks pile up,
425                // and the task deadlocks.
426                wake_user_data(user_data);
427            }
428        }
429    }
430}
431
432impl IO for UringIO {
433    fn supports_shared_wal_coordination(&self) -> bool {
434        true
435    }
436
437    fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
438        trace!("open_file(path = {})", path);
439        let mut file = std::fs::File::options();
440        file.read(true);
441
442        if !flags.contains(OpenFlags::ReadOnly) {
443            file.write(true);
444            file.create(flags.contains(OpenFlags::Create));
445        }
446
447        let file = file.open(path).map_err(|e| io_error(e, "open"))?;
448        // Let's attempt to enable direct I/O. Not all filesystems support it
449        // so ignore any errors.
450        let fd = file.as_fd();
451        if direct {
452            match fs::fcntl_setfl(fd, OFlags::DIRECT) {
453                Ok(_) => {}
454                Err(error) => debug!("Error {error:?} returned when setting O_DIRECT flag to read file. The performance of the system may be affected"),
455            }
456        }
457        let uring_file = Arc::new(UringFile {
458            ring: self.ring.clone(),
459            state: self.state.clone(),
460            caps: self.caps.clone(),
461            file,
462        });
463        if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err()
464            && !flags.intersects(OpenFlags::ReadOnly | OpenFlags::NoLock)
465        {
466            uring_file.lock_file(true)?;
467        }
468        Ok(uring_file)
469    }
470
471    fn remove_file(&self, path: &str) -> Result<()> {
472        std::fs::remove_file(path).map_err(|e| io_error(e, "remove_file"))?;
473        Ok(())
474    }
475
476    fn cancel(&self, completions: &[Completion]) -> Result<()> {
477        let mut state = self.state.lock();
478        for c in completions {
479            c.abort();
480            // dont want to leak the refcount bump with `get_key`/into_raw here, so we use as_ptr
481            let e = io_uring::opcode::AsyncCancel::new(Arc::as_ptr(c.get_inner()) as u64)
482                .build()
483                .user_data(CANCEL_TAG);
484            // SAFETY: holding `state` Mutex.
485            unsafe { state.submit_cancel_urgent(&self.ring, &e)? };
486        }
487        Ok(())
488    }
489
490    /// Drive io_uring forward.
491    ///
492    /// Leader/follower concurrency model:
493    /// - Submitters only take `state` (briefly, to push an SQE). They're
494    ///   never blocked by the kernel-side wait, so concurrent submitters
495    ///   pipeline their SQEs into the ring while another thread is
496    ///   waiting on the kernel.
497    /// - One thread at a time is the *leader*: it holds `wait_lock`
498    ///   and runs `submit_and_wait` + `drain_cq` in a tight loop until
499    ///   the ring drains. The drain *must* happen while the wait guard
500    ///   is still held — if released before draining, a follower could
501    ///   compute `pending_ops` to include CQEs we're about to consume
502    ///   and block forever on completions that no longer exist.
503    /// - Followers (concurrent callers of `step`) `try_lock` the
504    ///   `wait_lock`. If they lose the race they return immediately:
505    ///   the current leader will fire their waker as it drains. This
506    ///   replaces the previous "block on `wait_lock` and serialize"
507    ///   behavior, so N concurrent readers no longer queue up
508    ///   N-deep on the kernel call.
509    /// - Multiple concurrent kernel waiters on the same ring would
510    ///   themselves deadlock (Linux only wakes one waiter from the
511    ///   ring's wait queue when `min_complete` is reached), which is the
512    ///   other reason for serializing at this layer.
513    fn step(&self) -> Result<()> {
514        // Try to become the leader. If `wait_lock` is held, someone else
515        // is already inside `submit_and_wait`/`drain_cq` and will fire
516        // wakers on every completion drained: including ours. The
517        // follower returns Ok immediately and lets the calling Future
518        // park on its completion's waker.
519        let Some(_wait_guard) = self.wait_lock.try_lock() else {
520            return Ok(());
521        };
522
523        // Leader path: keep draining until the ring is empty. Looping
524        // here matters: while we were in the kernel, more submitters
525        // may have queued SQEs, and their futures need *us* to drain
526        // their CQEs before the calling task can make progress.
527        loop {
528            let pending = {
529                let mut state = self.state.lock();
530                // SAFETY: holding `state` Mutex.
531                unsafe { state.flush_overflow(&self.ring) };
532                if state.empty() {
533                    return Ok(());
534                }
535                state.pending_ops
536            };
537
538            let wants = std::cmp::min(pending, MAX_WAIT);
539            tracing::trace!("submit_and_wait for {wants} pending operations to complete");
540            self.ring
541                .submit_and_wait(wants)
542                .map_err(|e| io_error(e, "io_uring_submit_and_wait"))?;
543
544            // Drain while still holding `_wait_guard`.
545            self.drain_cq()?;
546        }
547    }
548
549    fn register_fixed_buffer(&self, ptr: std::ptr::NonNull<u8>, len: usize) -> Result<u32> {
550        turso_assert!(
551            len % 512 == 0,
552            "fixed buffer length must be logical block aligned"
553        );
554        let mut state = self.state.lock();
555        let slot =
556            state.free_arenas.iter().position(|e| e.is_none()).ok_or({
557                crate::error::CompletionError::UringIOError("no free fixed buffer slots")
558            })?;
559        unsafe {
560            self.ring
561                .submitter()
562                .register_buffers_update(
563                    slot as u32,
564                    &[libc::iovec {
565                        iov_base: ptr.as_ptr() as *mut libc::c_void,
566                        iov_len: len,
567                    }],
568                    None,
569                )
570                .map_err(|e| io_error(e, "register_buffers_update"))?
571        };
572        state.free_arenas[slot] = Some((ptr, len));
573        Ok(slot as u32)
574    }
575}
576
577impl UringIO {
578    /// Drain whatever CQEs are currently ready into completion callbacks.
579    /// Returns `true` if at least one CQE was processed.
580    fn drain_cq(&self) -> Result<bool> {
581        let mut state = self.state.lock();
582        let mut drained_any = false;
583        loop {
584            // SAFETY: holding `state` Mutex; no other CompletionQueue exists.
585            let mut cq = unsafe { self.ring.completion_shared() };
586            let Some(cqe) = cq.next() else {
587                return Ok(drained_any);
588            };
589            drained_any = true;
590            state.pending_ops -= 1;
591            let user_data = cqe.user_data();
592            if user_data == CANCEL_TAG {
593                continue;
594            }
595            let result = cqe.result();
596            turso_assert!(
597                user_data != 0,
598                "user_data must not be zero, we dont submit linked timeouts that would cause this"
599            );
600            if let Some(wstate) = state.writev_states.remove(&user_data) {
601                drop(cq);
602                // SAFETY: still holding `state` Mutex.
603                unsafe { state.handle_writev_completion(&self.ring, wstate, user_data, result) };
604                continue;
605            }
606            if result < 0 {
607                let errno = -result;
608                let err = std::io::Error::from_raw_os_error(errno);
609                completion_from_key(user_data)
610                    .error(CompletionError::IOError(err.kind(), "io_uring_cqe"));
611            } else {
612                completion_from_key(user_data).complete(result);
613            }
614        }
615    }
616}
617
618impl Clock for UringIO {
619    fn current_time_monotonic(&self) -> MonotonicInstant {
620        DefaultClock.current_time_monotonic()
621    }
622
623    fn current_time_wall_clock(&self) -> WallClockInstant {
624        DefaultClock.current_time_wall_clock()
625    }
626}
627
628#[inline(always)]
629/// use the callback pointer as the user_data for the operation as is
630/// common practice for io_uring to prevent more indirection
631fn get_key(c: Completion) -> u64 {
632    Arc::into_raw(c.get_inner().clone()) as u64
633}
634
635#[inline(always)]
636/// convert the user_data back to an Completion pointer
637fn completion_from_key(key: u64) -> Completion {
638    let c_inner = unsafe { Arc::from_raw(key as *const CompletionInner) };
639    Completion {
640        inner: Some(c_inner),
641    }
642}
643
644/// Wake the waker registered on the completion identified by `key` (and on
645/// its parent group, if any) without consuming the kernel's `Arc::into_raw`
646/// reference. Used to fire a progress wake when an operation is resubmitted
647/// (e.g., a writev split across chunks) so the future re-polls and continues
648/// draining the CQ.
649fn wake_user_data(key: u64) {
650    let ptr = key as *const CompletionInner;
651    // SAFETY: kernel owns one strong ref via the Arc::into_raw'd `key`. We
652    // re-materialize the Arc to clone it, then re-leak the original so the
653    // kernel's ref count is unchanged when this function returns.
654    let kernel_ref = unsafe { Arc::from_raw(ptr) };
655    let cloned = kernel_ref.clone();
656    let _ = Arc::into_raw(kernel_ref);
657    Completion {
658        inner: Some(cloned),
659    }
660    .wake_progress();
661}
662
663pub struct UringFile {
664    ring: Arc<io_uring::IoUring>,
665    state: Arc<Mutex<RingState>>,
666    caps: Arc<UringCapabilities>,
667    file: std::fs::File,
668}
669
670impl Deref for UringFile {
671    type Target = std::fs::File;
672    fn deref(&self) -> &Self::Target {
673        &self.file
674    }
675}
676
677unsafe impl Send for UringFile {}
678unsafe impl Sync for UringFile {}
679crate::assert::assert_send_sync!(UringFile);
680
681impl File for UringFile {
682    fn lock_file(&self, exclusive: bool) -> Result<()> {
683        let fd = self.file.as_fd();
684        // F_SETLK is a non-blocking lock. The lock will be released when the file is closed
685        // or the process exits or after an explicit unlock.
686        fs::fcntl_lock(
687            fd,
688            if exclusive {
689                FlockOperation::NonBlockingLockExclusive
690            } else {
691                FlockOperation::NonBlockingLockShared
692            },
693        )
694        .map_err(|e| {
695            let io_error = std::io::Error::from(e);
696            let message = match io_error.kind() {
697                ErrorKind::WouldBlock => {
698                    "Failed locking file. File is locked by another process".to_string()
699                }
700                _ => format!("Failed locking file, {io_error}"),
701            };
702            LimboError::LockingError(message)
703        })?;
704
705        Ok(())
706    }
707
708    fn unlock_file(&self) -> Result<()> {
709        let fd = self.file.as_fd();
710        fs::fcntl_lock(fd, FlockOperation::NonBlockingUnlock).map_err(|e| {
711            LimboError::LockingError(format!(
712                "Failed to release file lock: {}",
713                std::io::Error::from(e)
714            ))
715        })?;
716        Ok(())
717    }
718
719    fn pread(&self, pos: u64, c: Completion) -> Result<Completion> {
720        let r = c.as_read();
721        let read_e = {
722            let buf = r.buf();
723            let ptr = buf.as_mut_ptr();
724            let fd = io_uring::types::Fd(self.file.as_raw_fd());
725            let len = buf.len();
726            if let Some(idx) = buf.fixed_id() {
727                trace!(
728                    "pread_fixed(pos = {}, length = {}, idx = {})",
729                    pos,
730                    len,
731                    idx
732                );
733                #[cfg(debug_assertions)]
734                {
735                    self.state.lock().debug_check_fixed(idx, ptr, len);
736                }
737                io_uring::opcode::ReadFixed::new(fd, ptr, len as u32, idx as u16)
738                    .offset(pos)
739                    .build()
740                    .user_data(get_key(c.clone()))
741            } else {
742                trace!("pread(pos = {}, length = {})", pos, len);
743                io_uring::opcode::Read::new(fd, buf.as_mut_ptr(), len as u32)
744                    .offset(pos)
745                    .build()
746                    .user_data(get_key(c.clone()))
747            }
748        };
749        let mut state = self.state.lock();
750        // SAFETY: holding `state` Mutex.
751        unsafe { state.submit_entry(&self.ring, &read_e) };
752        Ok(c)
753    }
754
755    fn pwrite(&self, pos: u64, buffer: Arc<crate::Buffer>, c: Completion) -> Result<Completion> {
756        let write = {
757            let ptr = buffer.as_ptr();
758            let len = buffer.len();
759            let fd = io_uring::types::Fd(self.file.as_raw_fd());
760            if let Some(idx) = buffer.fixed_id() {
761                trace!(
762                    "pwrite_fixed(pos = {}, length = {}, idx= {})",
763                    pos,
764                    len,
765                    idx
766                );
767                #[cfg(debug_assertions)]
768                {
769                    self.state.lock().debug_check_fixed(idx, ptr, len);
770                }
771                io_uring::opcode::WriteFixed::new(fd, ptr, len as u32, idx as u16)
772                    .offset(pos)
773                    .build()
774                    .user_data(get_key(c.clone()))
775            } else {
776                trace!("pwrite(pos = {}, length = {})", pos, buffer.len());
777                io_uring::opcode::Write::new(fd, ptr, len as u32)
778                    .offset(pos)
779                    .build()
780                    .user_data(get_key(c.clone()))
781            }
782        };
783
784        // Keep the buffer alive until the completion is processed. For non-fixed
785        // buffers the SQE holds a raw pointer; without this the Arc would drop
786        // here and the kernel could read freed memory.
787        c.keep_write_buffer_alive(buffer);
788        let mut state = self.state.lock();
789        // SAFETY: holding `state` Mutex.
790        unsafe { state.submit_entry(&self.ring, &write) };
791        Ok(c)
792    }
793
794    fn sync(&self, c: Completion, _sync_type: crate::io::FileSyncType) -> Result<Completion> {
795        trace!("sync()");
796        let fd = io_uring::types::Fd(self.file.as_raw_fd());
797        let sync = io_uring::opcode::Fsync::new(fd)
798            .build()
799            .user_data(get_key(c.clone()));
800        let mut state = self.state.lock();
801        // SAFETY: holding `state` Mutex.
802        unsafe { state.submit_entry(&self.ring, &sync) };
803        Ok(c)
804    }
805
806    fn pwritev(
807        &self,
808        pos: u64,
809        bufs: Vec<Arc<crate::Buffer>>,
810        c: Completion,
811    ) -> Result<Completion> {
812        tracing::trace!("pwritev(pos = {}, bufs.len() = {})", pos, bufs.len());
813
814        let wstate = WritevState::new(self, pos, bufs);
815        let mut state = self.state.lock();
816        // SAFETY: holding `state` Mutex.
817        unsafe { state.submit_writev(&self.ring, get_key(c.clone()), wstate) };
818        Ok(c)
819    }
820
821    fn size(&self) -> Result<u64> {
822        Ok(self
823            .file
824            .metadata()
825            .map_err(|e| io_error(e, "metadata"))?
826            .len())
827    }
828
829    fn truncate(&self, len: u64, c: Completion) -> Result<Completion> {
830        let fd = io_uring::types::Fd(self.file.as_raw_fd());
831        if self.caps.ftruncate {
832            let truncate = io_uring::opcode::Ftruncate::new(fd, len)
833                .build()
834                .user_data(get_key(c.clone()));
835            let mut state = self.state.lock();
836            // SAFETY: holding `state` Mutex.
837            unsafe { state.submit_entry(&self.ring, &truncate) };
838            Ok(c)
839        } else {
840            let result = self.file.set_len(len);
841            match result {
842                Ok(()) => {
843                    trace!("file truncated to len=({})", len);
844                    c.complete(0);
845                    Ok(c)
846                }
847                Err(e) => Err(io_error(e, "truncate")),
848            }
849        }
850    }
851
852    fn shared_wal_lock_byte(
853        &self,
854        offset: u64,
855        exclusive: bool,
856        kind: SharedWalLockKind,
857    ) -> Result<()> {
858        unix_shared_wal_lock_byte(self.file.as_raw_fd(), offset, exclusive, true, kind).map(|_| ())
859    }
860
861    fn shared_wal_try_lock_byte(
862        &self,
863        offset: u64,
864        exclusive: bool,
865        kind: SharedWalLockKind,
866    ) -> Result<bool> {
867        unix_shared_wal_lock_byte(self.file.as_raw_fd(), offset, exclusive, false, kind)
868    }
869
870    fn shared_wal_unlock_byte(&self, offset: u64, kind: SharedWalLockKind) -> Result<()> {
871        unix_shared_wal_unlock_byte(self.file.as_raw_fd(), offset, kind)
872    }
873
874    fn shared_wal_set_len(&self, len: u64) -> Result<()> {
875        self.file
876            .set_len(len)
877            .map_err(|err| io_error(err, "resize shared WAL coordination file"))
878    }
879
880    fn shared_wal_map(&self, offset: u64, len: usize) -> Result<Box<dyn SharedWalMappedRegion>> {
881        unix_shared_wal_map(offset, len, self.file.as_raw_fd())
882    }
883}
884
885impl Drop for UringFile {
886    fn drop(&mut self) {
887        self.unlock_file().expect("Failed to unlock file");
888    }
889}
890
891#[cfg(clt_turso_tests)]
892mod tests {
893    use super::*;
894    use crate::io::common;
895
896    #[test]
897    fn test_multiple_processes_cannot_open_file() {
898        common::tests::test_multiple_processes_cannot_open_file(UringIO::new);
899    }
900}