fix(mount): track directory subdirectory count for correct nlink (#9028)

Track subdirectory count per-inode in memory via InodeEntry.subdirCount.
Increment on mkdir, decrement on rmdir, adjust on cross-directory
rename. applyDirNlink uses this count instead of listing metacache
entries, so nlink is correct immediately after mkdir without needing
a prior readdir.

Remove tests/rename/24.t from known_failures.txt (all 13 subtests
now pass).
This commit is contained in:
Chris Lu
2026-04-10 17:29:18 -07:00
committed by GitHub
parent ae724ac9d5
commit 066f7c3a0d
5 changed files with 61 additions and 16 deletions
-5
View File
@@ -6,11 +6,6 @@
# A failure in any test NOT listed here will cause the CI job to fail,
# catching regressions immediately.
# ── Directory nlink count before readdir ────────────────────────────────
# Directory nlink = 2 + subdirectory count is only accurate after the
# directory children have been cached (readdir). Before that, nlink=2.
tests/rename/24.t
# ── Directory rename permission edge case ──────────────────────────────
# Cross-directory rename of a subdirectory with restricted permissions
# causes cascading test failures within the test file.
+50
View File
@@ -27,6 +27,7 @@ type InodeEntry struct {
lastRefresh time.Time
updateWindowStart time.Time
updateCount int
subdirCount int32 // tracked in-memory for POSIX directory nlink
}
func (ie *InodeEntry) resetCacheState() {
@@ -250,6 +251,55 @@ func (i *InodeToPath) InvalidateChildrenCache(fullpath util.FullPath) {
entry.resetCacheState()
}
// AdjustSubdirCount adjusts the subdirectory count for a directory inode.
// delta is typically +1 (mkdir) or -1 (rmdir).
func (i *InodeToPath) AdjustSubdirCount(dirPath util.FullPath, delta int32) {
i.Lock()
defer i.Unlock()
inode, found := i.path2inode[dirPath]
if !found {
return
}
entry, found := i.inode2path[inode]
if !found || !entry.isDirectory {
return
}
entry.subdirCount += delta
if entry.subdirCount < 0 {
entry.subdirCount = 0
}
}
// GetSubdirCount returns the tracked subdirectory count for a directory.
func (i *InodeToPath) GetSubdirCount(dirPath util.FullPath) int32 {
i.RLock()
defer i.RUnlock()
inode, found := i.path2inode[dirPath]
if !found {
return 0
}
entry, found := i.inode2path[inode]
if !found || !entry.isDirectory {
return 0
}
return entry.subdirCount
}
// SetSubdirCount sets the subdirectory count for a directory (used after readdir).
func (i *InodeToPath) SetSubdirCount(dirPath util.FullPath, count int32) {
i.Lock()
defer i.Unlock()
inode, found := i.path2inode[dirPath]
if !found {
return
}
entry, found := i.inode2path[inode]
if !found || !entry.isDirectory {
return
}
entry.subdirCount = count
}
func (i *InodeToPath) TouchDirectory(fullpath util.FullPath) {
i.Lock()
defer i.Unlock()
+4 -11
View File
@@ -329,18 +329,11 @@ func (wfs *WFS) applyInMemoryAtime(out *fuse.Attr, inode uint64) {
}
// applyDirNlink sets nlink = 2 + number_of_subdirectories for a directory.
// Only counts from the local metacache to avoid expensive filer queries.
// When the cache has no entries (e.g. before readdir), keeps nlink=2.
// Uses the in-memory subdirectory count tracked by mkdir/rmdir/rename.
func (wfs *WFS) applyDirNlink(out *fuse.Attr, dirPath util.FullPath) {
var subdirCount uint32
wfs.metaCache.ListDirectoryEntries(context.Background(), dirPath, "", false, 100000, func(entry *filer.Entry) (bool, error) {
if entry.IsDirectory() {
subdirCount++
}
return true, nil
})
if subdirCount > 0 {
out.Nlink = 2 + subdirCount
count := wfs.inodeToPath.GetSubdirCount(dirPath)
if count > 0 {
out.Nlink = 2 + uint32(count)
}
}
+2
View File
@@ -78,6 +78,7 @@ func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out
}
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtime(dirFullPath)
wfs.inodeToPath.AdjustSubdirCount(dirFullPath, 1)
}
glog.V(3).Infof("mkdir %s: %v", entryFullPath, err)
@@ -155,6 +156,7 @@ func (wfs *WFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string
wfs.inodeToPath.RemovePath(entryFullPath)
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtime(dirFullPath)
wfs.inodeToPath.AdjustSubdirCount(dirFullPath, -1)
return fuse.OK
+5
View File
@@ -326,6 +326,11 @@ func (wfs *WFS) Rename(cancel <-chan struct{}, in *fuse.RenameIn, oldName string
wfs.touchDirMtimeCtime(oldDir)
if oldDir != newDir {
wfs.touchDirMtimeCtime(newDir)
// Adjust subdirectory counts when moving a directory across parents.
if oldEntry != nil && oldEntry.IsDirectory {
wfs.inodeToPath.AdjustSubdirCount(oldDir, -1)
wfs.inodeToPath.AdjustSubdirCount(newDir, 1)
}
}
return fuse.OK