diff --git a/src/client/dfuse/dfuse.h b/src/client/dfuse/dfuse.h index ab14572fa1f..45e9ab47d5f 100644 --- a/src/client/dfuse/dfuse.h +++ b/src/client/dfuse/dfuse.h @@ -1189,6 +1189,12 @@ ival_drop_inode(struct dfuse_inode_entry *inode); int ival_update_inode(struct dfuse_inode_entry *inode, double timeout); +/* Queue an on-demand dentry invalidation (parent/name) to be issued from the invalidation thread. + * ie_drop, if non-NULL, is a reference that is released after the invalidation has been issued. + */ +int +dfuse_mark_inval_entry(fuse_ino_t parent, const char *name, struct dfuse_inode_entry *ie_drop); + int ival_init(struct dfuse_info *dfuse_info); diff --git a/src/client/dfuse/inval.c b/src/client/dfuse/inval.c index 3ddcc052d86..505c6ecc862 100644 --- a/src/client/dfuse/inval.c +++ b/src/client/dfuse/inval.c @@ -1,5 +1,6 @@ /** * (C) Copyright 2016-2024 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * (C) Copyright 2025 Google LLC * * SPDX-License-Identifier: BSD-2-Clause-Patent @@ -91,10 +92,24 @@ struct dfuse_time_entry { /* Core data structure, maintains a list of struct dfuse_time_entry lists */ struct dfuse_ival { d_list_t time_entry_list; + struct dfuse_info *dfuse_info; struct fuse_session *session; bool session_dead; }; +/* A single on-demand dentry invalidation request. Request handlers running on the fixed worker + * pool enqueue these rather than calling fuse_lowlevel_notify_inval_entry() directly, as that + * blocks acquiring the parent's kernel i_rwsem, which may be held by a client waiting on the same + * worker pool - deadlocking it. The dedicated invalidation thread drains the queue instead. + */ +struct dfuse_inval_item { + d_list_t link; + fuse_ino_t parent; + char name[NAME_MAX + 1]; + /* Optional inode reference to drop once the invalidation has completed, or NULL */ + struct dfuse_inode_entry *ie_drop; +}; + /* The core data from struct dfuse_inode_entry. No additional inode references are held on inodes * because of there place on invalidate lists, rather inodes are removed from any list on close. * Therefore once a decision is made to evict an inode then a copy of the data is needed as once @@ -119,6 +134,9 @@ static pthread_t ival_thread; static sem_t ival_sem; static struct dfuse_ival ival_data; +/* On-demand invalidation queue, protected by ival_lock and drained by the invalidation thread */ +static d_list_t ival_queue; + /* Eviction loop, run periodically in it's own thread * * Returns true if there is more work to do. If false then *sleep_time is set in seconds. @@ -200,6 +218,69 @@ ival_loop(int *sleep_time) return (idx == EVICT_COUNT); } +/* Queue a dentry invalidation for the invalidation thread. Takes ownership of ie_drop, which is + * released after the invalidation has been issued. On failure ie_drop is not touched. + */ +int +dfuse_mark_inval_entry(fuse_ino_t parent, const char *name, struct dfuse_inode_entry *ie_drop) +{ + struct dfuse_inval_item *item; + + D_ALLOC_PTR(item); + if (item == NULL) + return ENOMEM; + + item->parent = parent; + item->ie_drop = ie_drop; + strncpy(item->name, name, NAME_MAX); + item->name[NAME_MAX] = '\0'; + + D_MUTEX_LOCK(&ival_lock); + d_list_add_tail(&item->link, &ival_queue); + D_MUTEX_UNLOCK(&ival_lock); + + sem_post(&ival_sem); + + return 0; +} + +/* Drain the on-demand invalidation queue. Runs on the invalidation thread so the blocking + * fuse_lowlevel_notify_inval_entry() call is issued off the worker pool that services /dev/fuse. + * Once shutdown has begun (ival_stop) items are freed without issuing notifies, as the worker pool + * is gone by then and a notify could block the thread indefinitely. + */ +static void +ival_drain_queue(void) +{ + struct dfuse_inval_item *item; + d_list_t drain; + + D_INIT_LIST_HEAD(&drain); + + /* Move the whole queue out under a single lock, then process without holding it. */ + D_MUTEX_LOCK(&ival_lock); + d_list_splice_init(&ival_queue, &drain); + D_MUTEX_UNLOCK(&ival_lock); + + while ((item = d_list_pop_entry(&drain, struct dfuse_inval_item, link)) != NULL) { + if (!ival_stop && !ival_data.session_dead) { + int rc; + + rc = fuse_lowlevel_notify_inval_entry(ival_data.session, item->parent, + item->name, + strnlen(item->name, NAME_MAX)); + if (rc && rc != -ENOENT && rc != -EBADF) + DHS_ERROR(&ival_data, -rc, "notify_inval_entry() failed"); + if (rc == -EBADF) + ival_data.session_dead = true; + } + + if (item->ie_drop) + dfuse_inode_decref(ival_data.dfuse_info, item->ie_drop); + D_FREE(item); + } +} + /* Main loop for eviction thread. Spins until ready for exit waking after one second and iterates * over all newly expired dentries. */ @@ -218,8 +299,10 @@ ival_thread_fn(void *arg) rc = sem_timedwait(&ival_sem, &ts); if (rc == 0) { - if (ival_stop) + if (ival_stop) { + ival_drain_queue(); return NULL; + } } else { rc = errno; @@ -227,6 +310,8 @@ ival_thread_fn(void *arg) DS_ERROR(rc, "sem_wait"); } + ival_drain_queue(); + while (ival_loop(&sleep_time)) ; if (sleep_time < 2) @@ -266,7 +351,9 @@ ival_init(struct dfuse_info *dfuse_info) DFUSE_TRA_UP(&ival_data, dfuse_info, "invalidator"); + ival_data.dfuse_info = dfuse_info; D_INIT_LIST_HEAD(&ival_data.time_entry_list); + D_INIT_LIST_HEAD(&ival_queue); rc = sem_init(&ival_sem, 0, 0); if (rc != 0) @@ -320,6 +407,16 @@ void ival_fini() { struct dfuse_time_entry *dte, *dtep; + struct dfuse_inval_item *item; + + /* Free any invalidation requests that were never issued. Any inode references held by + * queued items are released here. + */ + while ((item = d_list_pop_entry(&ival_queue, struct dfuse_inval_item, link)) != NULL) { + if (item->ie_drop) + dfuse_inode_decref(ival_data.dfuse_info, item->ie_drop); + D_FREE(item); + } /* Walk the list, oldest first */ d_list_for_each_entry_safe(dte, dtep, &ival_data.time_entry_list, dte_list) { diff --git a/src/client/dfuse/ops/lookup.c b/src/client/dfuse/ops/lookup.c index 1f1f25201b9..606008849c6 100644 --- a/src/client/dfuse/ops/lookup.c +++ b/src/client/dfuse/ops/lookup.c @@ -1,6 +1,6 @@ /** * (C) Copyright 2016-2024 Intel Corporation. - * (C) Copyright 2025 Hewlett Packard Enterprise Development LP + * (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -163,10 +163,9 @@ dfuse_reply_entry(struct dfuse_info *dfuse_info, struct dfuse_inode_entry *ie, if (wipe_parent == 0) return; - rc = fuse_lowlevel_notify_inval_entry(dfuse_info->di_session, wipe_parent, wipe_name, - strnlen(wipe_name, NAME_MAX)); - if (rc && rc != -ENOENT) - DS_ERROR(-rc, "inval_entry() failed"); + rc = dfuse_mark_inval_entry(wipe_parent, wipe_name, NULL); + if (rc) + DS_ERROR(rc, "dfuse_mark_inval_entry() failed"); return; out_err: diff --git a/src/client/dfuse/ops/open.c b/src/client/dfuse/ops/open.c index 5a5f7d3494e..580bef13a8a 100644 --- a/src/client/dfuse/ops/open.c +++ b/src/client/dfuse/ops/open.c @@ -242,12 +242,11 @@ dfuse_cb_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi) dfuse_inode_decref(dfuse_info, oh->doh_parent_dir); } if (ie) { - rc = fuse_lowlevel_notify_inval_entry(dfuse_info->di_session, ie->ie_parent, - ie->ie_name, strnlen(ie->ie_name, NAME_MAX)); - - if (rc != 0 && rc != -ENOENT) - DHS_ERROR(ie, -rc, "inval_entry() error"); - dfuse_inode_decref(dfuse_info, ie); + rc = dfuse_mark_inval_entry(ie->ie_parent, ie->ie_name, ie); + if (rc) { + DHS_ERROR(ie, rc, "dfuse_mark_inval_entry() failed"); + dfuse_inode_decref(dfuse_info, ie); + } } dfuse_oh_free(dfuse_info, oh); } diff --git a/src/client/dfuse/ops/opendir.c b/src/client/dfuse/ops/opendir.c index 60d1e6ab4d5..dc80b04664d 100644 --- a/src/client/dfuse/ops/opendir.c +++ b/src/client/dfuse/ops/opendir.c @@ -1,5 +1,6 @@ /** * (C) Copyright 2016-2024 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -83,12 +84,11 @@ dfuse_cb_releasedir(fuse_req_t req, struct dfuse_inode_entry *ino, struct fuse_f if (ie) { int rc; - rc = fuse_lowlevel_notify_inval_entry(dfuse_info->di_session, ie->ie_parent, - ie->ie_name, strnlen(ie->ie_name, NAME_MAX)); - - if (rc != 0 && rc != -ENOENT) - DHS_ERROR(ie, -rc, "inval_entry() error"); - dfuse_inode_decref(dfuse_info, ie); + rc = dfuse_mark_inval_entry(ie->ie_parent, ie->ie_name, ie); + if (rc) { + DHS_ERROR(ie, rc, "dfuse_mark_inval_entry() failed"); + dfuse_inode_decref(dfuse_info, ie); + } } dfuse_oh_free(dfuse_info, oh); }; diff --git a/src/client/dfuse/ops/rename.c b/src/client/dfuse/ops/rename.c index fcf95b82466..96fef8e2b8f 100644 --- a/src/client/dfuse/ops/rename.c +++ b/src/client/dfuse/ops/rename.c @@ -1,5 +1,6 @@ /** * (C) Copyright 2016-2023 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -34,11 +35,9 @@ dfuse_oid_moved(struct dfuse_info *dfuse_info, daos_obj_id_t *oid, struct dfuse_ (strncmp(ie->ie_name, name, NAME_MAX) != 0)) { DFUSE_TRA_DEBUG(ie, "Invalidating old name"); - rc = fuse_lowlevel_notify_inval_entry(dfuse_info->di_session, ie->ie_parent, - ie->ie_name, strnlen(ie->ie_name, NAME_MAX)); - - if (rc && rc != -ENOENT) - DFUSE_TRA_ERROR(ie, "inval_entry() returned: %d (%s)", rc, strerror(-rc)); + rc = dfuse_mark_inval_entry(ie->ie_parent, ie->ie_name, NULL); + if (rc) + DFUSE_TRA_ERROR(ie, "dfuse_mark_inval_entry() failed: %d", rc); } /* Update the inode entry data */ diff --git a/src/client/dfuse/ops/setxattr.c b/src/client/dfuse/ops/setxattr.c index e50c010b377..7682e9af1d4 100644 --- a/src/client/dfuse/ops/setxattr.c +++ b/src/client/dfuse/ops/setxattr.c @@ -1,5 +1,6 @@ /** * (C) Copyright 2019-2022 Intel Corporation. + * (C) Copyright 2026 Hewlett Packard Enterprise Development LP * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -47,21 +48,16 @@ dfuse_cb_setxattr(fuse_req_t req, struct dfuse_inode_entry *inode, rc = dfs_setxattr(inode->ie_dfs->dfs_ns, inode->ie_obj, name, value, size, flags); if (rc == 0) { /* Optionally remove the dentry to force a new lookup on access. - * If the xattr is to set a UNS entry point, and dentry_dir - * caching is enabled then invalidate the dentry here, to force - * a lookup which will check the xattr and return the linked - * container. The fuse header says this potentially deadlocks - * however it does appear to work, and calling this after the - * reply will introduce a race condition that future lookups - * will be skipped. + * If the xattr is to set a UNS entry point, and dentry_dir caching is enabled then + * invalidate the dentry to force a lookup which will check the xattr and return the + * linked container. The invalidation is queued to the invalidation thread rather + * than issued here as fuse_lowlevel_notify_inval_entry() blocks on kernel inode + * locks that may be held by a client waiting on this worker pool, deadlocking it. */ if (duns_attr && inode->ie_dfs->dfc_dentry_dir_timeout > 0) { - struct dfuse_info *dfuse_info = fuse_req_userdata(req); - - rc = fuse_lowlevel_notify_inval_entry(dfuse_info->di_session, - inode->ie_parent, inode->ie_name, - strnlen(inode->ie_name, NAME_MAX)); - DFUSE_TRA_INFO(inode, "inval_entry() rc is %d", rc); + rc = dfuse_mark_inval_entry(inode->ie_parent, inode->ie_name, NULL); + if (rc) + DHS_ERROR(inode, rc, "dfuse_mark_inval_entry() failed"); } DFUSE_REPLY_ZERO(inode, req); return;