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.
This commit is contained in:
Chris Lu
2026-08-20 10:09:42 -07:00
committed by GitHub
parent f7c4636d22
commit fc97f8ea8f
2 changed files with 136 additions and 55 deletions
+44 -55
View File
@@ -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)
+92
View File
@@ -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)
}