mergerfs/src/fuse_open.cpp

475 lines
13 KiB
C++
Raw Normal View History

2014-05-12 12:41:46 -04:00
/*
2026-02-23 13:22:21 -06:00
ISC License
Copyright (c) 2026, Antonio SJ Musumeci <trapexit@spawn.link>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2014-05-12 12:41:46 -04:00
*/
2026-01-19 15:16:01 -06:00
/*
2026-04-02 14:54:46 -05:00
OPEN FILE MANAGEMENT AND SHARED HANDLE TRACKING
===============================================
2026-01-19 15:16:01 -06:00
This file implements FUSE open() with handling for concurrent access
to the same file. The key challenge is that multiple threads may
simultaneously attempt to open the same file (same nodeid), and we
need to:
2026-04-02 14:54:46 -05:00
1. Share a single canonical FileInfo entry in the open_files map for all
opens of the same file while still giving each FUSE handle its own
FileInfo object and file descriptor
2026-01-19 15:16:01 -06:00
2. Properly reference count to know when to release resources
3. Handle race conditions without deadlocks or resource leaks
4. Support FUSE passthrough mode for performance
CONCURRENCY MODEL
-----------------
The open_files map (in state.hpp) tracks all currently open files:
- Key: nodeid (FUSE inode number)
- Value: OpenFile struct { ref_count, backing_id, fi }
When a file is opened:
1. First, we check if an entry already exists (using visit())
2026-04-02 14:54:46 -05:00
2. If it exists, we increment ref_count and create a new per-handle
FileInfo by re-opening the canonical file descriptor
2026-01-19 15:16:01 -06:00
3. If it doesn't exist, we create a new FileInfo and try to insert it
4. If insertion fails (another thread inserted first), we clean up
and retry (because to use passthrough it MUST be the same underlying
file as the backing id was created)
THE RETRY LOOP
--------------
The MAX_OPEN_RETRIES loop (lines ~294-380) handles the race where multiple
threads try to open the same file simultaneously:
Thread A: visit() -> no entry -> open file -> try_emplace() -> SUCCESS
Thread B: visit() -> no entry -> open file -> try_emplace() -> FAIL
Thread B: (cleans up, retries) -> visit() -> found Thread A's entry -> share it
This ensures that all threads eventually succeed while sharing the same
underlying file resources. The alternative is to not use passthrough
on failure but this race should be rare.
2026-04-02 14:54:46 -05:00
RELEASE INTERACTION
-------------------
2026-01-19 15:16:01 -06:00
2026-04-02 14:54:46 -05:00
release() erases the map entry on the final close. Until then, the
canonical FileInfo in the map remains valid and overlapping opens can bump
ref_count and duplicate its fd. Secondary handles own their own FileInfo
objects and are freed independently during release().
2026-01-19 15:16:01 -06:00
PASSTHROUGH MODE
----------------
FUSE passthrough (Linux 6.9+) allows the kernel to bypass mergerfs for I/O:
- We request a backing_id from the kernel via fuse_passthrough_open()
- If successful, subsequent reads/writes go directly to the backing file
- This achieves near native performance
The backing_id is stored in the OpenFile entry so all overlapping
open requests share the same passthrough handle.
*/
#include "fuse_open.hpp"
2025-06-05 23:47:42 -05:00
#include "state.hpp"
2014-05-12 12:41:46 -04:00
#include "config.hpp"
#include "errno.hpp"
#include "fileinfo.hpp"
2026-01-19 15:16:01 -06:00
#include "fuse_release.hpp"
#include "fs_close.hpp"
#include "fs_cow.hpp"
2020-08-17 16:29:22 -04:00
#include "fs_fchmod.hpp"
#include "fs_lchmod.hpp"
2020-08-17 16:29:22 -04:00
#include "fs_open.hpp"
2025-06-05 23:47:42 -05:00
#include "fs_open_fd.hpp"
#include "fs_openat.hpp"
#include "fs_path.hpp"
2020-08-17 16:29:22 -04:00
#include "fs_stat.hpp"
2025-06-05 23:47:42 -05:00
#include "fuse_passthrough.hpp"
#include "procfs.hpp"
#include "stat_util.hpp"
2014-05-12 12:41:46 -04:00
#include "fuse.h"
2026-04-02 14:54:46 -05:00
#include <sched.h>
#include <set>
#include <string>
#include <vector>
2026-01-19 15:16:01 -06:00
#define MAX_OPEN_RETRIES 64
2025-06-05 23:47:42 -05:00
static
bool
_rdonly(const int flags_)
{
return ((flags_ & O_ACCMODE) == O_RDONLY);
}
2025-06-05 23:47:42 -05:00
static
int
_lchmod_and_open_if_not_writable_and_empty(const fs::path &fullpath_,
const int flags_)
{
2025-06-05 23:47:42 -05:00
int rv;
struct stat st;
2025-06-05 23:47:42 -05:00
rv = fs::lstat(fullpath_,&st);
if(rv < 0)
return -EACCES;
2025-06-05 23:47:42 -05:00
if(StatUtil::writable(st))
return -EACCES;
2025-06-05 23:47:42 -05:00
rv = fs::lchmod(fullpath_,(st.st_mode|S_IWUSR|S_IWGRP));
if(rv < 0)
return -EACCES;
2025-06-05 23:47:42 -05:00
rv = fs::open(fullpath_,flags_);
if(rv < 0)
return -EACCES;
2025-06-05 23:47:42 -05:00
fs::fchmod(rv,st.st_mode);
2025-06-05 23:47:42 -05:00
return rv;
}
static
int
_nfsopenhack(const fs::path &fullpath_,
2025-06-05 23:47:42 -05:00
const int flags_,
const NFSOpenHack nfsopenhack_)
{
switch(nfsopenhack_)
{
default:
case NFSOpenHack::ENUM::OFF:
return -EACCES;
2025-06-05 23:47:42 -05:00
case NFSOpenHack::ENUM::GIT:
if(::_rdonly(flags_))
return -EACCES;
if(fullpath_.string().find("/.git/") == std::string::npos)
return -EACCES;
2025-06-05 23:47:42 -05:00
return ::_lchmod_and_open_if_not_writable_and_empty(fullpath_,flags_);
case NFSOpenHack::ENUM::ALL:
if(::_rdonly(flags_))
return -EACCES;
2025-06-05 23:47:42 -05:00
return ::_lchmod_and_open_if_not_writable_and_empty(fullpath_,flags_);
}
}
2025-06-05 23:47:42 -05:00
/*
The kernel expects being able to issue read requests when running
with writeback caching enabled so we must change O_WRONLY to
O_RDWR.
2025-06-05 23:47:42 -05:00
With writeback caching enabled the kernel handles O_APPEND. Could
be an issue if the underlying file changes out of band but that is
true of any caching.
*/
static
void
_tweak_flags_cache_writeback(int *flags_)
2025-06-05 23:47:42 -05:00
{
if((*flags_ & O_ACCMODE) == O_WRONLY)
*flags_ = ((*flags_ & ~O_ACCMODE) | O_RDWR);
if(*flags_ & O_APPEND)
*flags_ &= ~O_APPEND;
}
2025-06-05 23:47:42 -05:00
static
bool
_calculate_flush(FlushOnClose const flushonclose_,
int const flags_)
{
switch(flushonclose_)
{
case FlushOnCloseEnum::NEVER:
return false;
case FlushOnCloseEnum::OPENED_FOR_WRITE:
return !::_rdonly(flags_);
case FlushOnCloseEnum::ALWAYS:
return true;
}
return true;
}
2025-06-05 23:47:42 -05:00
static
void
_config_to_ffi_flags(Config &cfg_,
2025-06-05 23:47:42 -05:00
const int tid_,
fuse_file_info_t *ffi_)
{
switch(cfg.cache_files)
2025-06-05 23:47:42 -05:00
{
case CacheFiles::ENUM::OFF:
ffi_->direct_io = 1;
ffi_->keep_cache = 0;
ffi_->auto_cache = 0;
break;
case CacheFiles::ENUM::PARTIAL:
ffi_->direct_io = 0;
ffi_->keep_cache = 0;
ffi_->auto_cache = 0;
break;
case CacheFiles::ENUM::FULL:
ffi_->direct_io = 0;
ffi_->keep_cache = 1;
ffi_->auto_cache = 0;
break;
case CacheFiles::ENUM::AUTO_FULL:
ffi_->direct_io = 0;
ffi_->keep_cache = 0;
ffi_->auto_cache = 1;
break;
case CacheFiles::ENUM::PER_PROCESS:
std::string proc_name;
proc_name = procfs::get_name(tid_);
if(cfg.cache_files_process_names.count(proc_name) == 0)
2025-06-05 23:47:42 -05:00
{
ffi_->direct_io = 1;
ffi_->keep_cache = 0;
ffi_->auto_cache = 0;
}
else
{
ffi_->direct_io = 0;
ffi_->keep_cache = 0;
ffi_->auto_cache = 0;
}
break;
}
if(cfg.parallel_direct_writes == true)
2025-06-05 23:47:42 -05:00
ffi_->parallel_direct_writes = ffi_->direct_io;
}
2020-02-07 14:05:03 -05:00
2025-06-05 23:47:42 -05:00
static
int
_open_path(const fs::path &filepath_,
2025-06-05 23:47:42 -05:00
const Branch *branch_,
const fs::path &fusepath_,
2025-06-05 23:47:42 -05:00
fuse_file_info_t *ffi_,
const NFSOpenHack nfsopenhack_)
{
int fd;
FileInfo *fi;
2023-10-23 22:32:42 -05:00
2025-06-05 23:47:42 -05:00
fd = fs::openat(AT_FDCWD,filepath_,ffi_->flags);
if(fd == -EACCES)
2025-06-05 23:47:42 -05:00
fd = ::_nfsopenhack(filepath_,ffi_->flags,nfsopenhack_);
if(fd < 0)
return fd;
2020-02-07 14:05:03 -05:00
2025-06-05 23:47:42 -05:00
fi = new FileInfo(fd,branch_,fusepath_,ffi_->direct_io);
ffi_->fh = fi->to_fh();
2025-06-05 23:47:42 -05:00
return 0;
}
static
int
_open_fd(const int fd_,
const Branch *branch_,
const fs::path &fusepath_,
2025-06-05 23:47:42 -05:00
fuse_file_info_t *ffi_)
{
int fd;
FileInfo *fi;
2025-06-05 23:47:42 -05:00
fd = fs::open_fd(fd_,ffi_->flags);
if(fd < 0)
return fd;
2025-06-05 23:47:42 -05:00
fi = new FileInfo(fd,branch_,fusepath_,ffi_->direct_io);
ffi_->fh = fi->to_fh();
2025-06-05 23:47:42 -05:00
return 0;
}
static
int
_open(const Policy::Search &searchFunc_,
const Branches &ibranches_,
const fs::path &fusepath_,
2025-06-05 23:47:42 -05:00
fuse_file_info_t *ffi_,
const bool link_cow_,
const NFSOpenHack nfsopenhack_)
{
int rv;
fs::path filepath;
2025-06-05 23:47:42 -05:00
std::vector<Branch*> obranches;
2025-06-05 23:47:42 -05:00
rv = searchFunc_(ibranches_,fusepath_,obranches);
if(rv < 0)
return rv;
filepath = obranches[0]->path / fusepath_;
2025-06-05 23:47:42 -05:00
if(link_cow_ && fs::cow::is_eligible(filepath,ffi_->flags))
fs::cow::break_link(filepath);
2025-06-05 23:47:42 -05:00
rv = ::_open_path(filepath,
obranches[0],
fusepath_,
ffi_,
nfsopenhack_);
return rv;
}
constexpr
2026-03-01 13:58:06 -06:00
u64
_(const PassthroughIOEnum e_,
2026-03-01 13:58:06 -06:00
const u64 m_)
2025-06-05 23:47:42 -05:00
{
2026-03-01 13:58:06 -06:00
return ((((u64)e_) << 32) | (m_ & O_ACCMODE));
2025-06-05 23:47:42 -05:00
}
static
int
2026-01-19 15:16:01 -06:00
_open(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_)
2025-06-05 23:47:42 -05:00
{
int rv;
2026-01-19 15:16:01 -06:00
auto &of = state.open_files;
2025-06-05 23:47:42 -05:00
::_config_to_ffi_flags(cfg,ctx_->pid,ffi_);
if(cfg.cache_writeback)
::_tweak_flags_cache_writeback(&ffi_->flags);
2026-01-19 15:16:01 -06:00
ffi_->noflush = !::_calculate_flush(cfg.flushonclose,ffi_->flags);
2025-06-05 23:47:42 -05:00
2026-01-19 15:16:01 -06:00
for(size_t i = 0; i < MAX_OPEN_RETRIES; i++)
2025-06-05 23:47:42 -05:00
{
2026-01-19 15:16:01 -06:00
FileInfo *fi = nullptr;
int backing_id = INVALID_BACKING_ID;
// The ref increment keeps fuse_release.cpp from erasing and
2026-04-02 14:54:46 -05:00
// freeing the canonical entry while we duplicate its fd.
2026-01-19 15:16:01 -06:00
of.visit(ctx_->nodeid,
[&](auto &v_)
{
v_.second.ref_count++;
fi = v_.second.fi;
backing_id = v_.second.backing_id;
});
// If the file is already open...
if(fi)
2025-06-05 23:47:42 -05:00
{
2026-01-19 15:16:01 -06:00
rv = ::_open_fd(fi->fd,
&fi->branch,
fusepath_,
ffi_);
// If we fail to reopen the already open file we need to
// treat it similarly to fuse_release. Since we increased
// the ref count without an actual new fileinfo obj we call
// FUSE::release() without an obj. Nothing was allocated
// into ffi_->fh. _open_fd() failing should really never
// happen hence no retries. Just being careful.
if(rv < 0)
{
FUSE::release(ctx_->nodeid);
return rv;
}
switch(_(cfg.passthrough_io,ffi_->flags))
{
case _(PassthroughIO::ENUM::RO,O_RDONLY):
case _(PassthroughIO::ENUM::WO,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDONLY):
case _(PassthroughIO::ENUM::RW,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDWR):
if(not fuse_backing_id_is_valid(backing_id))
return 0;
ffi_->backing_id = backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
return 0;
}
return 0;
2025-06-05 23:47:42 -05:00
}
2026-01-19 15:16:01 -06:00
// Was not open, do first open, try to insert, if someone beat us
// to it in another thread then throw it away and try again.
rv = ::_open(cfg.func.open.policy,
cfg.branches,
fusepath_,
ffi_,
cfg.link_cow,
cfg.nfsopenhack);
if(rv < 0)
return rv;
2025-06-05 23:47:42 -05:00
2026-01-19 15:16:01 -06:00
fi = FileInfo::from_fh(ffi_->fh);
2025-06-05 23:47:42 -05:00
2026-01-19 15:16:01 -06:00
switch(_(cfg.passthrough_io,ffi_->flags))
{
case _(PassthroughIO::ENUM::RO,O_RDONLY):
case _(PassthroughIO::ENUM::WO,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDONLY):
case _(PassthroughIO::ENUM::RW,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDWR):
backing_id = FUSE::passthrough_open(fi->fd);
if(fuse_backing_id_is_valid(backing_id))
{
ffi_->backing_id = backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
}
break;
default:
break;
}
2025-06-05 23:47:42 -05:00
2026-01-19 15:16:01 -06:00
bool inserted = of.try_emplace(ctx_->nodeid,
backing_id,
fi);
if(inserted)
return 0;
// Failed to insert because another thread beat us to
// it. Cleanup the resources and try it all again.
FUSE::passthrough_close(backing_id);
fs::close(fi->fd);
delete fi;
2026-04-02 14:54:46 -05:00
ffi_->fh = 0;
::sched_yield();
2026-01-19 15:16:01 -06:00
}
2025-06-05 23:47:42 -05:00
2026-04-02 14:54:46 -05:00
return -EAGAIN;
2025-06-05 23:47:42 -05:00
}
int
FUSE::open(const fuse_req_ctx_t *ctx_,
const char *fusepath_,
fuse_file_info_t *ffi_)
2014-05-12 12:41:46 -04:00
{
const fs::path fusepath{fusepath_};
return ::_open(ctx_,fusepath,ffi_);
2014-05-12 12:41:46 -04:00
}