diff --git a/mkdocs/docs/config/deprecated_options.md b/mkdocs/docs/config/deprecated_options.md index 5ca25478..635a7fc8 100644 --- a/mkdocs/docs/config/deprecated_options.md +++ b/mkdocs/docs/config/deprecated_options.md @@ -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. diff --git a/mkdocs/docs/config/options.md b/mkdocs/docs/config/options.md index e30f28b5..e26535b9 100644 --- a/mkdocs/docs/config/options.md +++ b/mkdocs/docs/config/options.md @@ -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 diff --git a/mkdocs/docs/remote_filesystems.md b/mkdocs/docs/remote_filesystems.md index 4b8ebaca..35285956 100644 --- a/mkdocs/docs/remote_filesystems.md +++ b/mkdocs/docs/remote_filesystems.md @@ -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 diff --git a/mkdocs/docs/resource_usage.md b/mkdocs/docs/resource_usage.md index e82ffd1d..d1128267 100644 --- a/mkdocs/docs/resource_usage.md +++ b/mkdocs/docs/resource_usage.md @@ -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 diff --git a/src/config.cpp b/src/config.cpp index 012194c9..2194c1dc 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -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 = diff --git a/src/config.hpp b/src/config.hpp index c20cf102..0005a2e3 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -176,8 +176,8 @@ private: TFSRef _mountpoint; CfgNoforget _never_forget_nodes; CfgNoforget _noforget; - TFSRef _remember; - TFSRef _remember_nodes; + CfgNoforget _remember; + CfgNoforget _remember_nodes; SrcMounts _srcmounts; TFSRef _threads; TFSRef _uid; diff --git a/src/config_noforget.hpp b/src/config_noforget.hpp index 70ea021a..dbbd647c 100644 --- a/src/config_noforget.hpp +++ b/src/config_noforget.hpp @@ -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"; } diff --git a/tests/OPTIONS_TEST_PLAN.md b/tests/OPTIONS_TEST_PLAN.md index ae2e44be..80e1f153 100644 --- a/tests/OPTIONS_TEST_PLAN.md +++ b/tests/OPTIONS_TEST_PLAN.md @@ -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 | diff --git a/tests/tests.cpp b/tests/tests.cpp index ef7aac61..89ffee31 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -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}, diff --git a/vendored/libfuse/README.NFS b/vendored/libfuse/README.NFS index f3348d5a..2426348e 100644 --- a/vendored/libfuse/README.NFS +++ b/vendored/libfuse/README.NFS @@ -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. diff --git a/vendored/libfuse/include/fuse.h b/vendored/libfuse/include/fuse.h index d04969d2..c97d2062 100644 --- a/vendored/libfuse/include/fuse.h +++ b/vendored/libfuse/include/fuse.h @@ -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); diff --git a/vendored/libfuse/include/fuse_cfg.hpp b/vendored/libfuse/include/fuse_cfg.hpp index 1cedb2f5..cdd9d00e 100644 --- a/vendored/libfuse/include/fuse_cfg.hpp +++ b/vendored/libfuse/include/fuse_cfg.hpp @@ -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 log_file() const; diff --git a/vendored/libfuse/lib/fuse.cpp b/vendored/libfuse/lib/fuse.cpp index e761b77a..c2fce94c 100644 --- a/vendored/libfuse/lib/fuse.cpp +++ b/vendored/libfuse/lib/fuse.cpp @@ -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 diff --git a/vendored/libfuse/lib/node.hpp b/vendored/libfuse/lib/node.hpp index c04757d8..afc1b28c 100644 --- a/vendored/libfuse/lib/node.hpp +++ b/vendored/libfuse/lib/node.hpp @@ -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();