Skip to main content

clt_database/io/
unix.rs

1use super::{Completion, File, OpenFlags, SharedWalLockKind, SharedWalMappedRegion, IO};
2use crate::error::{io_error, CompletionError, LimboError};
3use crate::io::clock::{Clock, DefaultClock, MonotonicInstant, WallClockInstant};
4use crate::io::common;
5use crate::io::FileSyncType;
6use crate::Result;
7use rustix::{
8    fd::{AsFd, AsRawFd},
9    fs::{self, FlockOperation},
10};
11use std::os::fd::RawFd;
12use std::ptr::NonNull;
13
14use std::{io::ErrorKind, sync::Arc};
15#[cfg(clt_turso_feature = "fs")]
16use tracing::debug;
17use tracing::{instrument, trace, Level};
18
19// Darwin fails pwrite() and pwritev() calls with buffer size larger than INT_MAX so let's treat
20// that as maximum buffer size.
21const MAX_PWRITE_LEN: usize = i32::MAX as usize;
22
23const MAX_IOV: usize = 1024;
24
25pub struct UnixIO {}
26
27impl UnixIO {
28    #[cfg(clt_turso_feature = "fs")]
29    pub fn new() -> Result<Self> {
30        debug!("Using IO backend 'syscall'");
31        Ok(Self {})
32    }
33}
34
35impl Clock for UnixIO {
36    fn current_time_monotonic(&self) -> MonotonicInstant {
37        DefaultClock.current_time_monotonic()
38    }
39
40    fn current_time_wall_clock(&self) -> WallClockInstant {
41        DefaultClock.current_time_wall_clock()
42    }
43}
44
45impl IO for UnixIO {
46    fn supports_shared_wal_coordination(&self) -> bool {
47        true
48    }
49
50    fn open_file(&self, path: &str, flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
51        trace!("open_file(path = {})", path);
52        let mut file = std::fs::File::options();
53        file.read(true);
54
55        if !flags.contains(OpenFlags::ReadOnly) {
56            file.write(true);
57            file.create(flags.contains(OpenFlags::Create));
58        }
59
60        let file = file.open(path).map_err(|e| io_error(e, "open"))?;
61
62        #[allow(clippy::arc_with_non_send_sync)]
63        let unix_file = Arc::new(UnixFile {
64            file,
65            path: path.to_string(),
66        });
67        if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err()
68            && !flags.intersects(OpenFlags::ReadOnly | OpenFlags::NoLock)
69        {
70            unix_file.lock_file(true)?;
71        }
72        Ok(unix_file)
73    }
74
75    fn remove_file(&self, path: &str) -> Result<()> {
76        std::fs::remove_file(path).map_err(|e| io_error(e, "remove_file"))?;
77        Ok(())
78    }
79
80    #[instrument(err, skip_all, level = Level::TRACE)]
81    fn step(&self) -> Result<()> {
82        Ok(())
83    }
84}
85
86pub struct UnixFile {
87    file: std::fs::File,
88    path: String,
89}
90
91pub(crate) struct UnixSharedWalMapping {
92    mapping_ptr: NonNull<u8>,
93    mapping_len: usize,
94    ptr: NonNull<u8>,
95    len: usize,
96}
97
98unsafe impl Send for UnixSharedWalMapping {}
99unsafe impl Sync for UnixSharedWalMapping {}
100
101impl SharedWalMappedRegion for UnixSharedWalMapping {
102    fn ptr(&self) -> NonNull<u8> {
103        self.ptr
104    }
105
106    fn len(&self) -> usize {
107        self.len
108    }
109}
110
111impl Drop for UnixSharedWalMapping {
112    fn drop(&mut self) {
113        let rc = unsafe { libc::munmap(self.mapping_ptr.as_ptr().cast(), self.mapping_len) };
114        if rc != 0 {
115            // Log rather than panic — panicking in Drop aborts if we're already
116            // unwinding (double panic).
117            tracing::error!(
118                "munmap failed for shared WAL coordination region: {}",
119                std::io::Error::last_os_error()
120            );
121        }
122    }
123}
124
125pub(crate) fn unix_shared_wal_lock_byte(
126    fd: RawFd,
127    offset: u64,
128    exclusive: bool,
129    blocking: bool,
130    kind: SharedWalLockKind,
131) -> Result<bool> {
132    let mut flock = libc::flock {
133        l_type: if exclusive {
134            libc::F_WRLCK as libc::c_short
135        } else {
136            libc::F_RDLCK as libc::c_short
137        },
138        l_whence: libc::SEEK_SET as libc::c_short,
139        l_start: offset as libc::off_t,
140        l_len: 1,
141        l_pid: 0,
142        #[cfg(target_os = "freebsd")]
143        l_sysid: 0,
144    };
145    let cmd = match (kind, blocking) {
146        #[cfg(target_os = "linux")]
147        (SharedWalLockKind::LinuxOfd, true) => libc::F_OFD_SETLKW,
148        #[cfg(target_os = "linux")]
149        (SharedWalLockKind::LinuxOfd, false) => libc::F_OFD_SETLK,
150        (SharedWalLockKind::ProcessScopedFcntl, true) => libc::F_SETLKW,
151        (SharedWalLockKind::ProcessScopedFcntl, false) => libc::F_SETLK,
152        #[cfg(not(target_os = "linux"))]
153        (SharedWalLockKind::LinuxOfd, _) => {
154            return Err(LimboError::InternalError(
155                "linux OFD locks are not supported on this platform".into(),
156            ))
157        }
158    };
159    loop {
160        let rc = unsafe { libc::fcntl(fd, cmd, &mut flock) };
161        if rc == -1 {
162            let error = std::io::Error::last_os_error();
163            if blocking && error.kind() == ErrorKind::Interrupted {
164                continue;
165            }
166            if !blocking && error.kind() == ErrorKind::WouldBlock {
167                return Ok(false);
168            }
169            let message = match error.kind() {
170                ErrorKind::WouldBlock => {
171                    "Failed locking shared WAL coordination file. File is locked by another process"
172                        .to_string()
173                }
174                _ => format!("Failed locking shared WAL coordination file, {error}"),
175            };
176            return Err(LimboError::LockingError(message));
177        }
178        return Ok(true);
179    }
180}
181
182pub(crate) fn unix_shared_wal_unlock_byte(
183    fd: RawFd,
184    offset: u64,
185    kind: SharedWalLockKind,
186) -> Result<()> {
187    let mut flock = libc::flock {
188        l_type: libc::F_UNLCK as libc::c_short,
189        l_whence: libc::SEEK_SET as libc::c_short,
190        l_start: offset as libc::off_t,
191        l_len: 1,
192        l_pid: 0,
193        #[cfg(target_os = "freebsd")]
194        l_sysid: 0,
195    };
196    let cmd = match kind {
197        #[cfg(target_os = "linux")]
198        SharedWalLockKind::LinuxOfd => libc::F_OFD_SETLK,
199        SharedWalLockKind::ProcessScopedFcntl => libc::F_SETLK,
200        #[cfg(not(target_os = "linux"))]
201        SharedWalLockKind::LinuxOfd => {
202            return Err(LimboError::InternalError(
203                "linux OFD locks are not supported on this platform".into(),
204            ))
205        }
206    };
207    let rc = unsafe { libc::fcntl(fd, cmd, &mut flock) };
208    if rc == -1 {
209        Err(LimboError::LockingError(format!(
210            "Failed to release shared WAL coordination lock: {}",
211            std::io::Error::last_os_error()
212        )))
213    } else {
214        Ok(())
215    }
216}
217
218pub(crate) fn unix_shared_wal_map(
219    offset: u64,
220    len: usize,
221    fd: RawFd,
222) -> Result<Box<dyn SharedWalMappedRegion>> {
223    if len == 0 {
224        return Err(LimboError::InternalError(
225            "cannot mmap shared WAL coordination region with zero length".into(),
226        ));
227    }
228    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
229    if page_size <= 0 {
230        return Err(LimboError::LockingError(format!(
231            "failed to determine shared WAL mmap page size: {}",
232            std::io::Error::last_os_error()
233        )));
234    }
235    let page_size = page_size as u64;
236    let aligned_offset = offset / page_size * page_size;
237    let prefix_len = (offset - aligned_offset) as usize;
238    let mapping_len = prefix_len
239        .checked_add(len)
240        .ok_or_else(|| LimboError::InternalError("shared WAL mmap length overflow".into()))?;
241    let mapping_ptr = unsafe {
242        libc::mmap(
243            std::ptr::null_mut(),
244            mapping_len,
245            libc::PROT_READ | libc::PROT_WRITE,
246            libc::MAP_SHARED,
247            fd,
248            aligned_offset as libc::off_t,
249        )
250    };
251    if mapping_ptr == libc::MAP_FAILED {
252        let error = std::io::Error::last_os_error();
253        let file_size = unsafe {
254            let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
255            if libc::fstat(fd, stat.as_mut_ptr()) == 0 {
256                stat.assume_init().st_size
257            } else {
258                -1
259            }
260        };
261        return Err(LimboError::LockingError(format!(
262            "mmap shared WAL coordination file failed: {error} (offset={offset}, aligned_offset={aligned_offset}, len={len}, mapping_len={mapping_len}, fd={fd}, file_size={file_size})"
263        )));
264    }
265    let mapping_ptr =
266        NonNull::new(mapping_ptr.cast::<u8>()).expect("mmap returned null for shared WAL map");
267    let ptr = NonNull::new(unsafe { mapping_ptr.as_ptr().add(prefix_len) })
268        .expect("aligned mmap base plus prefix_len returned null");
269    Ok(Box::new(UnixSharedWalMapping {
270        mapping_ptr,
271        mapping_len,
272        ptr,
273        len,
274    }))
275}
276
277impl File for UnixFile {
278    fn lock_file(&self, exclusive: bool) -> Result<()> {
279        let fd = self.file.as_fd();
280        // F_SETLK is a non-blocking lock. The lock will be released when the file is closed
281        // or the process exits or after an explicit unlock.
282        fs::fcntl_lock(
283            fd,
284            if exclusive {
285                FlockOperation::NonBlockingLockExclusive
286            } else {
287                FlockOperation::NonBlockingLockShared
288            },
289        )
290        .map_err(|e| {
291            let io_error = std::io::Error::from(e);
292            let message = match io_error.kind() {
293                ErrorKind::WouldBlock => format!(
294                    "Failed locking file '{}'. File is locked by another process",
295                    self.path
296                ),
297                _ => format!("Failed locking file '{}', {io_error}", self.path),
298            };
299            LimboError::LockingError(message)
300        })?;
301
302        Ok(())
303    }
304
305    fn unlock_file(&self) -> Result<()> {
306        let fd = self.file.as_fd();
307        fs::fcntl_lock(fd, FlockOperation::NonBlockingUnlock).map_err(|e| {
308            LimboError::LockingError(format!(
309                "Failed to release file lock: {}",
310                std::io::Error::from(e)
311            ))
312        })?;
313        Ok(())
314    }
315
316    #[instrument(err, skip_all, level = Level::TRACE)]
317    fn pread(&self, pos: u64, c: Completion) -> Result<Completion> {
318        let result = unsafe {
319            let r = c.as_read();
320            let buf = r.buf();
321            let slice = buf.as_mut_slice();
322            libc::pread(
323                self.file.as_raw_fd(),
324                slice.as_mut_ptr() as *mut libc::c_void,
325                slice.len(),
326                pos as libc::off_t,
327            )
328        };
329        if result == -1 {
330            let e = std::io::Error::last_os_error();
331            Err(io_error(e, "pread"))
332        } else {
333            trace!("pread n: {}", result);
334            // Read succeeded immediately
335            c.complete(result as i32);
336            Ok(c)
337        }
338    }
339
340    #[instrument(err, skip_all, level = Level::TRACE)]
341    fn pwrite(&self, pos: u64, buffer: Arc<crate::Buffer>, c: Completion) -> Result<Completion> {
342        let buf_slice = buffer.as_slice();
343        let total_size = buf_slice.len();
344
345        let mut total_written = 0usize;
346        let mut current_pos = pos;
347
348        while total_written < total_size {
349            let remaining_slice = &buf_slice[total_written..];
350            let write_len = remaining_slice.len().min(MAX_PWRITE_LEN);
351            let result = unsafe {
352                libc::pwrite(
353                    self.file.as_raw_fd(),
354                    remaining_slice.as_ptr() as *const libc::c_void,
355                    write_len,
356                    current_pos as libc::off_t,
357                )
358            };
359            if result == -1 {
360                let e = std::io::Error::last_os_error();
361                if e.kind() == ErrorKind::Interrupted {
362                    // EINTR, retry without advancing
363                    continue;
364                }
365                return Err(io_error(e, "pwrite"));
366            }
367            let written = result as usize;
368            if written == 0 {
369                // Unexpected EOF for regular files
370                return Err(LimboError::CompletionError(CompletionError::IOError(
371                    ErrorKind::UnexpectedEof,
372                    "pwrite",
373                )));
374            }
375
376            total_written += written;
377            current_pos += written as u64;
378            trace!("pwrite iteration: wrote {written}, total {total_written}/{total_size}");
379        }
380        trace!("pwrite complete: wrote {total_written} bytes");
381        c.complete(total_written as i32);
382        Ok(c)
383    }
384
385    #[instrument(err, skip_all, level = Level::TRACE)]
386    fn pwritev(
387        &self,
388        pos: u64,
389        buffers: Vec<Arc<crate::Buffer>>,
390        c: Completion,
391    ) -> Result<Completion> {
392        if buffers.len().eq(&1) {
393            // use `pwrite` for single buffer
394            return self.pwrite(pos, buffers[0].clone(), c);
395        }
396
397        let total_size: usize = buffers.iter().map(|b| b.as_slice().len()).sum();
398        let mut iov: Vec<libc::iovec> = Vec::with_capacity(MAX_IOV);
399        let mut buf_idx = 0;
400        let mut buf_offset = 0;
401        let mut total_written = 0usize;
402        let mut current_pos = pos;
403
404        // This loop converts buffers into MAX_IOV iovecs, submits them for I/O, and runs again.
405        // If we we run out of iovecs before we convert a buffer in full, we keep track of buffer
406        // offset, and resume conversion from there.
407        loop {
408            while buf_idx < buffers.len() {
409                let buf = buffers[buf_idx].as_slice();
410                buf_offset += buf_to_iovecs(&buf[buf_offset..], &mut iov, MAX_IOV);
411                // If we ran out of iovecs, let's submit them for I/O.
412                if buf_offset < buf.len() {
413                    break;
414                }
415                // Buffer was fully conveted to iovec, move to next buffer.
416                buf_idx += 1;
417                buf_offset = 0;
418            }
419            if iov.is_empty() {
420                break;
421            }
422            let n = unsafe {
423                libc::pwritev(
424                    self.file.as_raw_fd(),
425                    iov.as_ptr(),
426                    iov.len() as i32,
427                    current_pos as libc::off_t,
428                )
429            };
430            if n < 0 {
431                let e = std::io::Error::last_os_error();
432                if e.kind() == ErrorKind::Interrupted {
433                    continue;
434                }
435                return Err(io_error(e, "pwritev"));
436            }
437            let written = n as usize;
438            if written == 0 {
439                return Err(LimboError::CompletionError(CompletionError::IOError(
440                    ErrorKind::UnexpectedEof,
441                    "pwritev",
442                )));
443            }
444            total_written += written;
445            current_pos += written as u64;
446            trim_iovecs(&mut iov, written);
447            trace!("pwritev iteration: wrote {written}, total {total_written}/{total_size}");
448        }
449        trace!("pwritev complete: wrote {total_written} bytes");
450        c.complete(total_written as i32);
451        Ok(c)
452    }
453
454    #[instrument(err, skip_all, level = Level::TRACE)]
455    fn sync(&self, c: Completion, sync_type: FileSyncType) -> Result<Completion> {
456        let result = unsafe {
457            #[cfg(target_vendor = "apple")]
458            {
459                match sync_type {
460                    FileSyncType::Fsync => libc::fsync(self.file.as_raw_fd()),
461                    FileSyncType::FullFsync => {
462                        libc::fcntl(self.file.as_raw_fd(), libc::F_FULLFSYNC)
463                    }
464                }
465            }
466            #[cfg(not(target_vendor = "apple"))]
467            {
468                // FullFsync has no effect on non-Apple platforms
469                let _ = sync_type;
470                libc::fsync(self.file.as_raw_fd())
471            }
472        };
473
474        if result == -1 {
475            let e = std::io::Error::last_os_error();
476            Err(io_error(e, "sync"))
477        } else {
478            #[cfg(target_vendor = "apple")]
479            match sync_type {
480                FileSyncType::FullFsync => trace!("fcntl(F_FULLFSYNC)"),
481                FileSyncType::Fsync => trace!("fsync"),
482            }
483            #[cfg(not(target_vendor = "apple"))]
484            trace!("fsync");
485
486            c.complete(0);
487            Ok(c)
488        }
489    }
490
491    #[instrument(err, skip_all, level = Level::TRACE)]
492    fn size(&self) -> Result<u64> {
493        Ok(self
494            .file
495            .metadata()
496            .map_err(|e| io_error(e, "metadata"))?
497            .len())
498    }
499
500    #[instrument(err, skip_all, level = Level::DEBUG)]
501    fn truncate(&self, len: u64, c: Completion) -> Result<Completion> {
502        let result = self.file.set_len(len);
503        match result {
504            Ok(()) => {
505                trace!("file truncated to len=({})", len);
506                c.complete(0);
507                Ok(c)
508            }
509            Err(e) => Err(io_error(e, "truncate")),
510        }
511    }
512
513    fn shared_wal_lock_byte(
514        &self,
515        offset: u64,
516        exclusive: bool,
517        kind: SharedWalLockKind,
518    ) -> Result<()> {
519        unix_shared_wal_lock_byte(self.file.as_raw_fd(), offset, exclusive, true, kind).map(|_| ())
520    }
521
522    fn shared_wal_try_lock_byte(
523        &self,
524        offset: u64,
525        exclusive: bool,
526        kind: SharedWalLockKind,
527    ) -> Result<bool> {
528        unix_shared_wal_lock_byte(self.file.as_raw_fd(), offset, exclusive, false, kind)
529    }
530
531    fn shared_wal_unlock_byte(&self, offset: u64, kind: SharedWalLockKind) -> Result<()> {
532        unix_shared_wal_unlock_byte(self.file.as_raw_fd(), offset, kind)
533    }
534
535    fn shared_wal_set_len(&self, len: u64) -> Result<()> {
536        self.file
537            .set_len(len)
538            .map_err(|err| io_error(err, "resize shared WAL coordination file"))
539    }
540
541    fn shared_wal_map(&self, offset: u64, len: usize) -> Result<Box<dyn SharedWalMappedRegion>> {
542        unix_shared_wal_map(offset, len, self.file.as_raw_fd())
543    }
544}
545
546/// Append iovec entries for `buf` to `iovecs`, splitting `buf` into chunks of
547/// at most `MAX_PWRITE_LEN` bytes. Stops once `iovecs.len()` reaches `max_iovecs`.
548/// Returns the number of bytes consumed from `buf`.
549fn buf_to_iovecs(buf: &[u8], iovecs: &mut Vec<libc::iovec>, max_iovecs: usize) -> usize {
550    let mut slice = buf;
551    while !slice.is_empty() && iovecs.len() < max_iovecs {
552        let chunk_len = slice.len().min(MAX_PWRITE_LEN);
553        iovecs.push(libc::iovec {
554            iov_base: slice.as_ptr() as *mut libc::c_void,
555            iov_len: chunk_len,
556        });
557        slice = &slice[chunk_len..];
558    }
559    buf.len() - slice.len()
560}
561
562/// Drop the first `n` bytes from the front of `iov`, advancing the leading
563/// entry's pointer if a partial iovec was consumed.
564fn trim_iovecs(iov: &mut Vec<libc::iovec>, mut n: usize) {
565    let mut idx = 0;
566    while idx < iov.len() {
567        if iov[idx].iov_len > n {
568            break;
569        }
570        n -= iov[idx].iov_len;
571        idx += 1;
572    }
573    iov.drain(..idx);
574    if n > 0 {
575        assert!(!iov.is_empty());
576        let front = &mut iov[0];
577        front.iov_base = unsafe { (front.iov_base as *mut u8).add(n) as *mut libc::c_void };
578        front.iov_len -= n;
579    }
580}
581
582impl Drop for UnixFile {
583    fn drop(&mut self) {
584        self.unlock_file().expect("Failed to unlock file");
585    }
586}
587
588#[cfg(clt_turso_tests)]
589mod tests {
590    use super::*;
591    use std::io::Write;
592
593    #[test]
594    fn test_multiple_processes_cannot_open_file() {
595        common::tests::test_multiple_processes_cannot_open_file(UnixIO::new);
596    }
597
598    #[test]
599    fn test_shared_wal_map_supports_unaligned_logical_offset() {
600        let file = tempfile::NamedTempFile::new().unwrap();
601        let backing_len = 128 * 1024;
602        let bytes: Vec<u8> = (0..backing_len).map(|i| (i % 251) as u8).collect();
603        file.as_file().write_all(&bytes).unwrap();
604        file.as_file().sync_all().unwrap();
605
606        let mapped = unix_shared_wal_map(4096, 81920, file.as_file().as_raw_fd()).unwrap();
607        assert_eq!(mapped.len(), 81920);
608        let slice = unsafe { std::slice::from_raw_parts(mapped.ptr().as_ptr(), mapped.len()) };
609        assert_eq!(&slice[..128], &bytes[4096..4096 + 128]);
610        assert_eq!(&slice[mapped.len() - 128..], &bytes[4096 + 81920 - 128..4096 + 81920]);
611    }
612}