From fc97f8ea8f9eadc3f0573b9d4b9d6b23a7248448 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 20 Aug 2026 10:09:42 -0700 Subject: [PATCH] mount: index directory state by path (#10827) Every directory-state lookup went through path2inode, the map that holds one full path per inode in the table, and then through dirStates. Directories now carry their own path and are indexed by it directly. There are orders of magnitude fewer directories than files, so this map stays small whatever the mount holds, and it is what a file needs before it can stop carrying a full path of its own: a child's path is its parent's plus its name. No behavior change - the two indexes are asserted to agree. --- weed/mount/inode_to_path.go | 99 ++++++++++------------- weed/mount/inode_to_path_dirstate_test.go | 92 +++++++++++++++++++++ 2 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 weed/mount/inode_to_path_dirstate_test.go diff --git a/weed/mount/inode_to_path.go b/weed/mount/inode_to_path.go index 705658ba0..6f0bc8890 100644 --- a/weed/mount/inode_to_path.go +++ b/weed/mount/inode_to_path.go @@ -18,6 +18,9 @@ type InodeToPath struct { // dirStates holds directory-only readdir-cache state, keyed by inode. An // inode is a directory iff it has an entry here, registered at creation. dirStates map[uint64]*dirState + // dirPaths indexes the same states by path, so a directory lookup never + // goes through the map that holds one full path per inode. + dirPaths map[util.FullPath]*dirState } // InodeEntry exists per inode the kernel references. Directory cache state is @@ -31,6 +34,7 @@ type InodeEntry struct { } type dirState struct { + path util.FullPath isChildrenCached bool readDirDirect bool cachedExpiresTime time.Time @@ -126,13 +130,14 @@ func NewInodeToPath(root util.FullPath, ttlSec int) *InodeToPath { inode2path: make(map[uint64]*InodeEntry), path2inode: make(map[util.FullPath]uint64), dirStates: make(map[uint64]*dirState), + dirPaths: make(map[util.FullPath]*dirState), cacheMetaTtlSec: time.Second * time.Duration(ttlSec), } t.inode2path[1] = &InodeEntry{ path: root, nlookup: 1, } - t.dirStates[1] = &dirState{lastAccess: time.Now()} + t.setDirState(1, &dirState{path: root, lastAccess: time.Now()}) t.path2inode[root] = 1 return t @@ -183,7 +188,7 @@ func (i *InodeToPath) Lookup(path util.FullPath, unixTime int64, isDirectory boo nlookup: nlookup, } if isDirectory { - i.dirStates[inode] = &dirState{} + i.setDirState(inode, &dirState{path: path}) } } @@ -269,6 +274,25 @@ func (i *InodeToPath) GetAllPaths(inode uint64) []util.FullPath { return ie.appendPaths(nil) } +func (i *InodeToPath) setDirState(inode uint64, d *dirState) { + i.dirStates[inode] = d + i.dirPaths[d.path] = d +} + +// dropDirPath drops the path index only; Forget releases the state itself. A +// released directory's state keeps the path it had, so drop the index only +// while it is still the one that path resolves to: a new directory may have +// taken the name in the meantime. +func (i *InodeToPath) dropDirPath(inode uint64) { + if d := i.dirStates[inode]; d != nil && i.dirPaths[d.path] == d { + delete(i.dirPaths, d.path) + } +} + +func (i *InodeToPath) dirStateOf(fullpath util.FullPath) *dirState { + return i.dirPaths[fullpath] +} + func (i *InodeToPath) HasPath(path util.FullPath) bool { i.RLock() defer i.RUnlock() @@ -279,16 +303,11 @@ func (i *InodeToPath) HasPath(path util.FullPath) bool { func (i *InodeToPath) MarkChildrenCached(fullpath util.FullPath) { i.Lock() defer i.Unlock() - inode, found := i.path2inode[fullpath] - if !found { + d := i.dirStateOf(fullpath) + if d == nil { // https://github.com/seaweedfs/seaweedfs/issues/4968 // glog.Fatalf("MarkChildrenCached not found inode %v", fullpath) - glog.Warningf("MarkChildrenCached not found inode %v", fullpath) - return - } - d, found := i.dirStates[inode] - if !found { - glog.Warningf("MarkChildrenCached inode %d not a tracked directory for %v", inode, fullpath) + glog.Warningf("MarkChildrenCached not a tracked directory: %v", fullpath) return } d.isChildrenCached = true @@ -304,11 +323,7 @@ func (i *InodeToPath) MarkChildrenCached(fullpath util.FullPath) { func (i *InodeToPath) IsChildrenCached(fullpath util.FullPath) bool { i.RLock() defer i.RUnlock() - inode, found := i.path2inode[fullpath] - if !found { - return false - } - d := i.dirStates[inode] + d := i.dirStateOf(fullpath) if d == nil { return false } @@ -341,11 +356,7 @@ func (i *InodeToPath) InvalidateAllChildrenCache() { func (i *InodeToPath) InvalidateChildrenCache(fullpath util.FullPath) { i.Lock() defer i.Unlock() - inode, found := i.path2inode[fullpath] - if !found { - return - } - if d := i.dirStates[inode]; d != nil { + if d := i.dirStateOf(fullpath); d != nil { d.resetCacheState() } } @@ -355,11 +366,7 @@ func (i *InodeToPath) InvalidateChildrenCache(fullpath util.FullPath) { func (i *InodeToPath) AdjustSubdirCount(dirPath util.FullPath, delta int32) { i.Lock() defer i.Unlock() - inode, found := i.path2inode[dirPath] - if !found { - return - } - d := i.dirStates[inode] + d := i.dirStateOf(dirPath) if d == nil { return } @@ -373,11 +380,7 @@ func (i *InodeToPath) AdjustSubdirCount(dirPath util.FullPath, delta int32) { func (i *InodeToPath) GetSubdirCount(dirPath util.FullPath) int32 { i.RLock() defer i.RUnlock() - inode, found := i.path2inode[dirPath] - if !found { - return 0 - } - d := i.dirStates[inode] + d := i.dirStateOf(dirPath) if d == nil { return 0 } @@ -388,11 +391,7 @@ func (i *InodeToPath) GetSubdirCount(dirPath util.FullPath) int32 { func (i *InodeToPath) SetSubdirCount(dirPath util.FullPath, count int32) { i.Lock() defer i.Unlock() - inode, found := i.path2inode[dirPath] - if !found { - return - } - if d := i.dirStates[inode]; d != nil { + if d := i.dirStateOf(dirPath); d != nil { d.subdirCount = count } } @@ -400,11 +399,7 @@ func (i *InodeToPath) SetSubdirCount(dirPath util.FullPath, count int32) { func (i *InodeToPath) TouchDirectory(fullpath util.FullPath) { i.Lock() defer i.Unlock() - inode, found := i.path2inode[fullpath] - if !found { - return - } - if d := i.dirStates[inode]; d != nil { + if d := i.dirStateOf(fullpath); d != nil { d.lastAccess = time.Now() } } @@ -412,11 +407,7 @@ func (i *InodeToPath) TouchDirectory(fullpath util.FullPath) { func (i *InodeToPath) MarkDirectoryReadThrough(fullpath util.FullPath, now time.Time) bool { i.Lock() defer i.Unlock() - inode, found := i.path2inode[fullpath] - if !found { - return false - } - d := i.dirStates[inode] + d := i.dirStateOf(fullpath) if d == nil { return false } @@ -431,11 +422,7 @@ func (i *InodeToPath) MarkDirectoryReadThrough(fullpath util.FullPath, now time. func (i *InodeToPath) ShouldReadDirectoryDirect(fullpath util.FullPath) bool { i.RLock() defer i.RUnlock() - inode, found := i.path2inode[fullpath] - if !found { - return false - } - d := i.dirStates[inode] + d := i.dirStateOf(fullpath) if d == nil { return false } @@ -445,11 +432,7 @@ func (i *InodeToPath) ShouldReadDirectoryDirect(fullpath util.FullPath) bool { func (i *InodeToPath) MarkDirectoryRefreshed(fullpath util.FullPath, now time.Time) { i.Lock() defer i.Unlock() - inode, found := i.path2inode[fullpath] - if !found { - return - } - d := i.dirStates[inode] + d := i.dirStateOf(fullpath) if d == nil { return } @@ -506,6 +489,7 @@ func (i *InodeToPath) RemovePath(path util.FullPath) { inode, found := i.path2inode[path] if found { delete(i.path2inode, path) + i.dropDirPath(inode) i.removePathFromInode2Path(inode, path) } } @@ -535,12 +519,16 @@ func (i *InodeToPath) MovePath(sourcePath, targetPath util.FullPath) (sourceInod if targetFound { i.removePathFromInode2Path(targetInode, targetPath) delete(i.path2inode, targetPath) + i.dropDirPath(targetInode) } delete(i.path2inode, sourcePath) i.path2inode[targetPath] = sourceInode if entry, entryFound := i.inode2path[sourceInode]; entryFound { entry.replacePath(sourcePath, targetPath) if d := i.dirStates[sourceInode]; d != nil { + i.dropDirPath(sourceInode) + d.path = targetPath + i.dirPaths[targetPath] = d d.resetCacheState() } } else { @@ -582,6 +570,7 @@ func (i *InodeToPath) Forget(inode, nlookup uint64, onRelease func(inode uint64) } } delete(i.inode2path, inode) + i.dropDirPath(inode) delete(i.dirStates, inode) } else { glog.V(4).Infof("kernel forget but nlookup not zero: inode %d path %v nlookup %d", inode, path.path, path.nlookup) diff --git a/weed/mount/inode_to_path_dirstate_test.go b/weed/mount/inode_to_path_dirstate_test.go new file mode 100644 index 000000000..dc75e6847 --- /dev/null +++ b/weed/mount/inode_to_path_dirstate_test.go @@ -0,0 +1,92 @@ +package mount + +import ( + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/util" +) + +// The two indexes hold the same states, at the path the inode entry carries. +func checkDirIndexes(t *testing.T, itp *InodeToPath) { + t.Helper() + itp.RLock() + defer itp.RUnlock() + for path, d := range itp.dirPaths { + if d.path != path { + t.Errorf("dirPaths[%s] holds a state for %s", path, d.path) + } + } + for inode, d := range itp.dirStates { + e := itp.inode2path[inode] + if e == nil || e.path == "" { + continue + } + if e.path != d.path { + t.Errorf("inode %d: entry at %s, directory state at %s", inode, e.path, d.path) + } + if itp.dirPaths[d.path] != d { + t.Errorf("inode %d: directory state at %s is missing from dirPaths", inode, d.path) + } + } +} + +func TestDirStateIndexesStayInStep(t *testing.T) { + itp := NewInodeToPath(util.FullPath("/"), 0) + now := time.Now().Unix() + + dirInode := itp.Lookup("/a", now, true, false, 0, true) + subInode := itp.Lookup("/a/sub", now, true, false, 0, true) + itp.Lookup("/a/sub/f.txt", now, false, false, 0, true) + checkDirIndexes(t, itp) + + if itp.dirStateOf("/a/sub") == nil { + t.Fatal("/a/sub not indexed as a directory") + } + if itp.dirStateOf("/a/sub/f.txt") != nil { + t.Error("a file landed in the directory index") + } + + itp.MovePath("/a/sub", "/a/moved") + checkDirIndexes(t, itp) + if itp.dirStateOf("/a/sub") != nil { + t.Error("pre-rename directory still indexed") + } + if itp.dirStateOf("/a/moved") == nil { + t.Error("renamed directory not indexed") + } + + itp.RemovePath("/a/moved") + if itp.dirStateOf("/a/moved") != nil { + t.Error("removed directory still indexed") + } + checkDirIndexes(t, itp) + + itp.Forget(subInode, 1, nil, nil) + itp.Forget(dirInode, 1, nil, nil) + checkDirIndexes(t, itp) + if itp.dirStateOf("/a") != nil { + t.Error("forgotten directory still indexed") + } +} + +// A released directory's state keeps its old path. Forgetting it must not take +// the index entry of whatever holds that name now. +func TestForgetDoesNotDropAReusedDirPath(t *testing.T) { + itp := NewInodeToPath(util.FullPath("/"), 0) + now := time.Now().Unix() + + oldInode := itp.Lookup("/a", now, true, false, 0, true) + itp.RemovePath("/a") + newInode := itp.Lookup("/a", now+1, true, false, 0, true) + if newInode == oldInode { + t.Fatalf("recreated directory reused inode %d", newInode) + } + + itp.Forget(oldInode, 1, nil, nil) + + if itp.dirStateOf("/a") == nil { + t.Error("live directory lost its index when the released one was forgotten") + } + checkDirIndexes(t, itp) +}