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
19const 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 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 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 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 continue;
364 }
365 return Err(io_error(e, "pwrite"));
366 }
367 let written = result as usize;
368 if written == 0 {
369 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 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 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 buf_offset < buf.len() {
413 break;
414 }
415 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 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
546fn 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
562fn 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}