Remove timed node removal

This commit is contained in:
Antonio SJ Musumeci
2026-05-06 20:49:46 -04:00
committed by trapexit
parent 8117998205
commit f1034a3ad6
14 changed files with 176 additions and 257 deletions
+1
View File
@@ -15,6 +15,7 @@ function or have been replaced. **They should not be used.**
* **hard_remove**: Does nothing.
* **kernel_cache**: Use [cache.files=full](cache.md).
* **negative_entry**: Use [cache.negative-entry](cache.md).
* **remember**: Use `never-forget-nodes`.
* **noforget**: Use `never-forget-nodes`.
* **nonempty**: Does nothing.
* **splice_move**: Does nothing.
+5 -6
View File
@@ -213,12 +213,11 @@ config file.
* **passthrough.max-stack-depth=INT**: Set to `1` another filesystem
can be stacked on mergerfs. Set to `2` to have mergerfs stacked over
another filesystem. (default: 1)
* **remember-nodes=INT**: The number of seconds to keep the internal
representation of a file once the OS tells mergerfs it is no longer
needed. Really only needed for [exporting mergerfs via
NFS](../remote_filesystems.md) (default: 0)
* **never-forget-nodes=BOOL**: Sets `remember-nodes` to
infinity. Formerly `noforget`. (default: false)
* **never-forget-nodes=BOOL**: Keep the internal representation of
a file after the OS tells mergerfs it is no longer needed, until the
node is unlinked, removed, or replaced. Really only needed for
[exporting mergerfs via NFS](../remote_filesystems.md). Formerly
`noforget`. (default: false)
* **allow-idmap=BOOL**: Enables idmap (identity mapping). (default:
true)
* **debug**: Enable debugging information. At the moment limited to a
+5 -6
View File
@@ -57,7 +57,7 @@ NFS export settings:
* `softreval`: NFS client will serve up cached data after `retrans`
attempts to revalidate the data. Helps with intermittent network
issues.
* `timeo=150`: Timeout till retrying request.
* `timeo=150`: Timeout till retrying request.
* `retrans=3`: Number of retrying a request.
* `rsize=131072`: Read size.
* `wsize=131072`: Write size.
@@ -76,12 +76,11 @@ but if you run into problems it may be worth trying Samba/SMB.
**mergerfs settings:**
* `never-forget-nodes=true` or `remember-nodes=SECONDS`
* `never-forget-nodes=true`
* `inodecalc=path-hash`
* `lazy-umount-mountpoint=false`
`never-forget-nodes=true` (or `remember-nodes` with a large value such
as `86400` for a full day) is needed because NFS uses the
`never-forget-nodes=true` is needed because NFS uses the
`name_to_handle_at` and `open_by_handle_at` functions which allow a
program to keep a reference to a file without technically having it
open in the typical sense. The problem is that FUSE has no way to know
@@ -89,8 +88,8 @@ that NFS has a handle that it will later use to open the file
again. As a result, it is possible for the kernel to tell mergerfs to
forget about the file node and should NFS ever ask for that node's
details in the future it would have nothing to respond with. Keeping
nodes around forever (or however long is set with `remember-nodes`) is
not ideal but at the moment the only way to manage the situation.
nodes around until they are unlinked, removed, or replaced is not ideal
but at the moment the only way to manage the situation.
`inodecalc=path-hash` is needed because NFS is sensitive to
out-of-band changes. FUSE doesn't care if a file's inode value changes
+1 -1
View File
@@ -12,7 +12,7 @@
depending on [fuse-msg-size](config/fuse-msg-size.md)
* buffers allocated temporarily for reading directories
* FUSE nodes
* never-forget-nodes=true nodes
* never-forget-nodes=true retained nodes, until unlinked or removed
## Management
+12 -11
View File
@@ -135,10 +135,6 @@ Config::Config()
_max_background(fuse_cfg.max_background),
_mount(mountpoint),
_mountpoint(mountpoint),
_never_forget_nodes(),
_noforget(),
_remember(fuse_cfg.remember_nodes),
_remember_nodes(fuse_cfg.remember_nodes),
_srcmounts(branches),
_threads(fuse_cfg.read_thread_count),
_uid(fuse_cfg.uid),
@@ -147,13 +143,22 @@ Config::Config()
_initialized(false)
{
allow_idmap.ro =
_congestion_threshold.ro =
_max_background.ro =
_mount.ro =
_mountpoint.ro =
_never_forget_nodes.ro =
_noforget.ro =
_remember.ro =
_remember_nodes.ro =
_srcmounts.ro =
_version.ro =
allow_idmap.ro =
async_read.ro =
branches_mount_timeout.ro =
branches_mount_timeout_fail.ro =
cache_symlinks.ro =
cache_writeback.ro =
_congestion_threshold.ro =
direct_io_allow_mmap.ro =
export_support.ro =
fsname.ro =
@@ -161,9 +166,6 @@ Config::Config()
handle_killpriv.ro =
handle_killpriv_v2.ro =
kernel_permissions_check.ro =
_max_background.ro =
_mount.ro =
_mountpoint.ro =
nullrw.ro =
pid.ro =
pin_threads.ro =
@@ -172,8 +174,6 @@ Config::Config()
process_thread_queue_depth.ro =
read_thread_count.ro =
scheduling_priority.ro =
_srcmounts.ro =
_version.ro =
true;
_congestion_threshold.display =
_gid.display =
@@ -181,6 +181,7 @@ Config::Config()
_mount.display =
_noforget.display =
_remember.display =
_remember_nodes.display =
_srcmounts.display =
_threads.display =
_uid.display =
+2 -2
View File
@@ -176,8 +176,8 @@ private:
TFSRef<fs::path> _mountpoint;
CfgNoforget _never_forget_nodes;
CfgNoforget _noforget;
TFSRef<s64> _remember;
TFSRef<s64> _remember_nodes;
CfgNoforget _remember;
CfgNoforget _remember_nodes;
SrcMounts _srcmounts;
TFSRef<int> _threads;
TFSRef<s64> _uid;
+15 -9
View File
@@ -32,18 +32,24 @@ public:
{
int rv;
bool b = false;
s64 i = 0;
rv = str::from(s_,&b);
if((b == true) || s_.empty())
if(s_.empty())
{
fuse_cfg.remember_nodes = -1;
return 0;
b = true;
}
else if((rv = str::from(s_,&i)) == 0)
{
b = (i != 0);
}
else
{
rv = str::from(s_,&b);
if(rv)
return rv;
}
if(rv)
return rv;
fuse_cfg.remember_nodes = 0;
fuse_cfg.remember_nodes = b;
return 0;
}
@@ -51,7 +57,7 @@ public:
std::string
to_string() const
{
if(fuse_cfg.remember_nodes == -1)
if(fuse_cfg.remember_nodes)
return "true";
return "false";
}
+2 -2
View File
@@ -100,8 +100,8 @@ Legend:
| Option | Integration tests | Unit tests |
|---|---|---|
| `lazy-umount-mountpoint` | Live-upgrade remount scenario with stale previous mount | Startup control flow and error handling |
| `remember-nodes` | NFS-like lookup stability over configured retention interval | Node cache retention/expiry logic |
| `noforget` | No node expiry over extended period | Infinite retention mode mapping |
| `remember-nodes` | NFS-like lookup stability until unlink/remove/rename replacement | Boolean and numeric compatibility parsing |
| `noforget` | Retained nodes released on unlink/remove/rename replacement | Alias mapping to retained-node mode |
| `allow-idmap` | UID/GID mapping behavior in idmapped mounts | Init flag propagation and compatibility checks |
| `debug` | FUSE trace generation enabled/disabled | Debug option parse and logger toggling |
| `log.file` | Trace output to stderr vs file path | Path parser and file sink setup |
+56
View File
@@ -2730,6 +2730,61 @@ test_config_set_kv_form()
TEST_CHECK(val == "true");
}
void
test_config_remember_nodes_bool_numeric()
{
bool old_remember_nodes = fuse_cfg.remember_nodes;
Config cfg;
std::string val;
fuse_cfg.remember_nodes = false;
// remember-nodes accepts ints: 0 disables, non-zero enables
TEST_CHECK(cfg.set("remember-nodes", "0") == 0);
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
TEST_CHECK(val == "false");
TEST_CHECK(cfg.set("remember-nodes", "86400") == 0);
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
TEST_CHECK(val == "true");
TEST_CHECK(cfg.set("remember", "-1") == 0);
TEST_CHECK(cfg.get("remember", &val) == 0);
TEST_CHECK(val == "true");
// remember-nodes also accepts bool strings
TEST_CHECK(cfg.set("remember-nodes", "false") == 0);
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
TEST_CHECK(val == "false");
// never-forget-nodes is bool; it reflects the feature state
TEST_CHECK(cfg.set("never-forget-nodes", "true") == 0);
TEST_CHECK(cfg.get("never-forget-nodes", &val) == 0);
TEST_CHECK(val == "true");
// Querying remember-nodes after never-forget-nodes=true returns true
// (both alias the same fuse_cfg.remember_nodes)
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
TEST_CHECK(val == "true");
// Empty string treated as true
TEST_CHECK(cfg.set("noforget", "") == 0);
TEST_CHECK(cfg.get("noforget", &val) == 0);
TEST_CHECK(val == "true");
// remember-nodes also accepts empty string as true
fuse_cfg.remember_nodes = false;
TEST_CHECK(cfg.set("remember-nodes", "") == 0);
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
TEST_CHECK(val == "true");
// Invalid string rejected by remember-nodes
TEST_CHECK(cfg.set("remember-nodes", "notabool") == -EINVAL);
fuse_cfg.remember_nodes = old_remember_nodes;
}
void
test_config_readonly_before_init()
{
@@ -3683,6 +3738,7 @@ TEST_LIST =
{"config_set_underscore_normalization",test_config_set_underscore_normalization},
{"config_get_underscore_normalization",test_config_get_underscore_normalization},
{"config_set_kv_form",test_config_set_kv_form},
{"config_remember_nodes_bool_numeric",test_config_remember_nodes_bool_numeric},
{"config_readonly_before_init",test_config_readonly_before_init},
{"config_readonly_after_init",test_config_readonly_after_init},
{"config_mutable_after_init",test_config_mutable_after_init},
+5 -4
View File
@@ -27,7 +27,8 @@ Because the high-level interface is path based, it is not possible to
delegate looking up by inode to the filesystem.
To work around this, currently a "noforget" option is provided, which
makes the library remember nodes forever. This will make the NFS
server happy, but also results in an ever growing memory footprint for
the filesystem. For this reason if the filesystem is large (or the
memory is small), then this option is not recommended.
makes the library remember nodes until they are unlinked or otherwise
removed. This will make the NFS server happy, but also results in a
larger memory footprint for the filesystem. For this reason if the
filesystem is large (or the memory is small), then this option is not
recommended.
+2 -3
View File
@@ -365,13 +365,12 @@ void fuse_populate_maintenance_thread(struct fuse *fuse);
/**
* Iterate over cache removing stale entries
* use in conjunction with "-oremember"
* Legacy cache cleanup entry point.
*
* NOTE: This is already done for the standard sessions
*
* @param fuse struct fuse pointer for fuse instance
* @return the number of seconds until the next cleanup
* @return 0
*/
int fuse_clean_cache(struct fuse *fuse);
+1 -1
View File
@@ -25,7 +25,7 @@ struct fuse_cfg_t
s64 umask = FUSE_CFG_INVALID_UMASK;
bool valid_umask() const;
s64 remember_nodes = 0;
bool remember_nodes = false;
bool debug = false;
std::shared_ptr<FILE> log_file() const;
+61 -211
View File
@@ -102,13 +102,6 @@ struct list_head
struct list_head *prev;
};
typedef struct remembered_node_t remembered_node_t;
struct remembered_node_t
{
node_t *node;
time_t time;
};
typedef struct nodeid_gen_t nodeid_gen_t;
struct nodeid_gen_t
{
@@ -127,7 +120,6 @@ struct fuse
fuse_operations ops;
struct lock_queue_element *lockq;
kvec_t(remembered_node_t) remembered_nodes;
};
#define TREELOCK_WRITE -1
@@ -192,20 +184,6 @@ get_node(const uint64_t nodeid)
return NULL;
}
static
void
remove_remembered_node(node_t *node_)
{
for(size_t i = 0; i < kv_size(f.remembered_nodes); i++)
{
if(kv_A(f.remembered_nodes,i).node != node_)
continue;
kv_delete(f.remembered_nodes,i);
break;
}
}
static
uint32_t
stat_crc32b(const struct stat *st_)
@@ -230,31 +208,6 @@ stat_crc32b(const struct stat *st_)
return (crc ? crc : UINT32_MAX);
}
#ifndef CLOCK_MONOTONIC
# define CLOCK_MONOTONIC CLOCK_REALTIME
#endif
static
time_t
current_time()
{
int rv;
struct timespec now;
static clockid_t clockid = CLOCK_MONOTONIC;
rv = clock_gettime(clockid,&now);
if((rv == -1) && (errno == EINVAL))
{
clockid = CLOCK_REALTIME;
rv = clock_gettime(clockid,&now);
}
if(rv == -1)
now.tv_sec = time(NULL);
return now.tv_sec;
}
static
void
free_node(node_t *node_)
@@ -560,10 +513,10 @@ hash_name(node_t *node,
static
inline
int
bool
remember_nodes()
{
return (fuse_cfg.remember_nodes > 0);
return fuse_cfg.remember_nodes;
}
static
@@ -572,8 +525,6 @@ delete_node(node_t *node)
{
assert(node->treelock == 0);
unhash_name(node);
if(remember_nodes())
remove_remembered_node(node);
unhash_id(node);
node_free(node);
}
@@ -626,10 +577,45 @@ inc_nlookup(node_t *node)
node->nlookup++;
}
static
void
remember_node(node_t *node)
{
if(node->remembered)
return;
inc_nlookup(node);
node->remembered = true;
}
static
void
unref_node_if_unused(node_t *node)
{
if((node->nlookup == 0) &&
(node->open_count == 0) &&
(node->treelock == 0))
unref_node(node);
}
static
void
unremember_node(node_t *node)
{
if(!node->remembered)
return;
assert(node->nlookup > 0);
node->remembered = false;
node->nlookup--;
unref_node_if_unused(node);
}
static
node_t*
find_node(uint64_t parent,
const char *name)
const char *name,
bool remember)
{
node_t *node;
mutex_lockguard(f.lock);
@@ -652,8 +638,8 @@ find_node(uint64_t parent,
goto out_err;
node->nodeid = generate_nodeid(&f.nodeid_gen);
if(fuse_cfg.remember_nodes)
inc_nlookup(node);
if(remember && remember_nodes())
remember_node(node);
if(hash_name(node,parent,name) == -1)
{
@@ -663,10 +649,6 @@ find_node(uint64_t parent,
}
hash_id(node);
}
else if((node->nlookup == 1) && remember_nodes())
{
remove_remembered_node(node);
}
inc_nlookup(node);
out_err:
@@ -725,6 +707,8 @@ unlock_path(uint64_t nodeid,
{
assert(wnode->treelock == TREELOCK_WRITE);
wnode->treelock = 0;
/* unlink_node() can drop the retained lookup while the node is locked. */
unref_node_if_unused(wnode);
}
node = get_node(nodeid);
@@ -1147,31 +1131,15 @@ forget_node(const uint64_t nodeid,
assert(node->nlookup >= nlookup);
node->nlookup -= nlookup;
if(node->nlookup == 0)
{
unref_node(node);
}
else if((node->nlookup == 1) && remember_nodes())
{
remembered_node_t fn;
fn.node = node;
fn.time = current_time();
kv_push(remembered_node_t,f.remembered_nodes,fn);
}
unref_node_if_unused(node);
}
static
void
unlink_node(node_t *node)
{
if(remember_nodes())
{
assert(node->nlookup > 1);
node->nlookup--;
}
unhash_name(node);
unremember_node(node);
}
static
@@ -1205,7 +1173,7 @@ rename_node(uint64_t olddir,
if(node == NULL)
goto out;
if(newnode != NULL)
if((newnode != NULL) && (newnode != node))
unlink_node(newnode);
unhash_name(node);
@@ -1260,11 +1228,12 @@ static
int
set_path_info(uint64_t nodeid,
const char *name,
struct fuse_entry_param *e)
struct fuse_entry_param *e,
bool remember = true)
{
node_t *node;
node = find_node(nodeid,name);
node = find_node(nodeid,name,remember);
if(node == NULL)
return -ENOMEM;
@@ -1296,7 +1265,8 @@ lookup_path(fuse_req_t *req_,
const char *name,
const char *fusepath,
struct fuse_entry_param *e,
fuse_file_info_t *fi)
fuse_file_info_t *fi,
bool remember = true)
{
int rv;
@@ -1311,7 +1281,7 @@ lookup_path(fuse_req_t *req_,
if(rv)
return rv;
return set_path_info(nodeid,name,e);
return set_path_info(nodeid,name,e,remember);
}
static
@@ -2043,6 +2013,7 @@ fuse_do_release(fuse_req_ctx_t *req_ctx_,
{
assert(node->open_count > 0);
node->open_count--;
unref_node_if_unused(node);
}
}
}
@@ -2070,12 +2041,12 @@ fuse_lib_create(fuse_req_t *req_,
else
name = ((char*)arg + sizeof(struct fuse_open_in));
// opportunistically allocate a node so we have a new nodeid for
// later in the actual create. Added for use with passthrough.
// Opportunistically allocate a provisional node so passthrough create has
// a nodeid. It is remembered only after create fully succeeds.
{
node_t *node;
node = find_node(hdr_->nodeid,name);
node = find_node(hdr_->nodeid,name,false);
if(node == NULL)
{
fuse_reply_err(req_,ENOMEM);
@@ -2095,7 +2066,7 @@ fuse_lib_create(fuse_req_t *req_,
&ffi);
if(!err)
{
err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,&ffi);
err = lookup_path(req_,hdr_->nodeid,name,fusepath,&e,&ffi,false);
if(err)
{
f.ops.release(&req_->ctx,&ffi);
@@ -2114,7 +2085,11 @@ fuse_lib_create(fuse_req_t *req_,
mutex_lock(f.lock);
node_t *node = get_node(e.ino);
if(node)
node->open_count++;
{
if(remember_nodes())
remember_node(node);
node->open_count++;
}
mutex_unlock(f.lock);
if(fuse_reply_create(req_,&e,&ffi) == -ENOENT)
@@ -3184,122 +3159,6 @@ fuse_lib_fallocate(fuse_req_t *req_,
fuse_reply_err(req_,err);
}
static
int
remembered_node_cmp(const void *a_,
const void *b_)
{
const remembered_node_t *a = (const remembered_node_t*)a_;
const remembered_node_t *b = (const remembered_node_t*)b_;
if(a->time < b->time)
return -1;
if(a->time > b->time)
return 1;
return 0;
}
static
void
remembered_nodes_sort()
{
mutex_lock(f.lock);
qsort(&kv_first(f.remembered_nodes),
kv_size(f.remembered_nodes),
sizeof(remembered_node_t),
remembered_node_cmp);
mutex_unlock(f.lock);
}
#define MAX_PRUNE 100
#define MAX_CHECK 1000
int
fuse_prune_some_remembered_nodes(int *offset_)
{
time_t now;
int pruned;
int checked;
mutex_lock(f.lock);
pruned = 0;
checked = 0;
now = current_time();
while(*offset_ < (int)kv_size(f.remembered_nodes))
{
time_t age;
remembered_node_t *fn = &kv_A(f.remembered_nodes,*offset_);
if(pruned >= MAX_PRUNE)
break;
if(checked >= MAX_CHECK)
break;
checked++;
age = (now - fn->time);
if(fuse_cfg.remember_nodes > age)
break;
assert(fn->node->nlookup == 1);
/* Don't forget active directories */
if(fn->node->refctr > 1)
{
(*offset_)++;
continue;
}
fn->node->nlookup = 0;
unref_node(fn->node);
kv_delete(f.remembered_nodes,*offset_);
pruned++;
}
mutex_unlock(f.lock);
if((pruned < MAX_PRUNE) && (checked < MAX_CHECK))
*offset_ = -1;
return pruned;
}
#undef MAX_PRUNE
#undef MAX_CHECK
static
void
sleep_100ms(void)
{
const struct timespec ms100 = {0,100 * 1000000};
nanosleep(&ms100,NULL);
}
void
fuse_prune_remembered_nodes()
{
int offset;
int pruned;
offset = 0;
pruned = 0;
for(;;)
{
pruned += fuse_prune_some_remembered_nodes(&offset);
if(offset >= 0)
{
sleep_100ms();
continue;
}
break;
}
if(pruned > 0)
remembered_nodes_sort();
}
static struct fuse_lowlevel_ops fuse_path_ops =
{
.access = fuse_lib_access,
@@ -3532,12 +3391,6 @@ fuse_gc1()
void
fuse_populate_maintenance_thread(struct fuse *f_)
{
MaintenanceThread::push_job([=](u64 count_)
{
if(remember_nodes())
fuse_prune_remembered_nodes();
});
MaintenanceThread::push_job([](u64 count_)
{
if((count_ % 15) == 0)
@@ -3582,8 +3435,6 @@ fuse_new(int fd_,
mutex_init(f.lock);
kv_init(f.remembered_nodes);
root = node_alloc();
if(root == NULL)
{
@@ -3636,7 +3487,6 @@ fuse_destroy(struct fuse *)
free(f.name_table.array);
mutex_destroy(f.lock);
fuse_session_destroy(f.se);
kv_destroy(f.remembered_nodes);
}
int
+8 -1
View File
@@ -13,11 +13,18 @@ struct node_t
uint64_t nlookup;
uint32_t refctr;
uint32_t open_count;
uint32_t open_count : 31;
uint32_t remembered : 1;
uint32_t stat_crc32b;
int32_t treelock;
};
#if defined(__LP64__) || defined(_LP64)
static_assert(sizeof(node_t) == 64, "node_t expected to be 64 bytes on 64-bit");
#else
static_assert(sizeof(node_t) == 48, "node_t expected to be 48 bytes on 32-bit");
#endif
node_t *node_alloc();
void node_free(node_t*);
void node_gc();