diff --git a/weed/command/mount.go b/weed/command/mount.go index 5784e80da..e153c7c6a 100644 --- a/weed/command/mount.go +++ b/weed/command/mount.go @@ -21,6 +21,7 @@ type MountOptions struct { concurrentReaders *int cacheMetaTtlSec *int cacheDirMaxEntries *int + maxInodeEntries *int cacheDirForRead *string cacheDirForWrite *string cacheSizeMBForRead *int64 @@ -116,6 +117,7 @@ func init() { mountOptions.writeBufferSizeMB = cmdMount.Flag.Int64("writeBufferSizeMB", 0, "global cap on the per-mount write buffer (memory + swap) in MB, 0 means unlimited. Bounds /tmp growth when volume uploads stall") mountOptions.cacheMetaTtlSec = cmdMount.Flag.Int("cacheMetaTtlSec", 60, "metadata cache validity seconds") mountOptions.cacheDirMaxEntries = cmdMount.Flag.Int("cacheDirMaxEntries", 100000, "a directory with more children than this is not cached locally but read directly from the filer; 0 caches everything") + mountOptions.maxInodeEntries = cmdMount.Flag.Int("maxInodeEntries", 0, "stop handing the kernel references from readdirplus once this many inodes are tracked; listings stay complete and the kernel looks up what it needs. Does not evict - the table shrinks only as the kernel returns references. 0 is unlimited") mountOptions.dataCenter = cmdMount.Flag.String("dataCenter", "", "prefer to write to the data center") mountOptions.allowOthers = cmdMount.Flag.Bool("allowOthers", true, "allows other users to access the file system") mountOptions.defaultPermissions = cmdMount.Flag.Bool("defaultPermissions", true, "enforce permissions by the operating system") diff --git a/weed/command/mount_common.go b/weed/command/mount_common.go index c3ac11d3b..da2cd738f 100644 --- a/weed/command/mount_common.go +++ b/weed/command/mount_common.go @@ -204,6 +204,7 @@ func buildSeaweedFileSystem(option *MountOptions, p fileSystemParams) *mount.WFS WriteBufferSizeMB: *option.writeBufferSizeMB, CacheMetaTTlSec: *option.cacheMetaTtlSec, CacheDirMaxEntries: *option.cacheDirMaxEntries, + MaxInodeEntries: *option.maxInodeEntries, DataCenter: *option.dataCenter, Quota: int64(*option.collectionQuota) * 1024 * 1024, LogicalDiskUsage: *option.logicalDiskUsage, diff --git a/weed/mount/inode_to_path.go b/weed/mount/inode_to_path.go index 3a6595835..b63c6b7a3 100644 --- a/weed/mount/inode_to_path.go +++ b/weed/mount/inode_to_path.go @@ -214,6 +214,12 @@ func (i *InodeToPath) GetAllPaths(inode uint64) []util.FullPath { return out } +func (i *InodeToPath) Len() int { + i.RLock() + defer i.RUnlock() + return len(i.inode2path) +} + func (i *InodeToPath) HasPath(path util.FullPath) bool { i.RLock() defer i.RUnlock() diff --git a/weed/mount/weedfs.go b/weed/mount/weedfs.go index 44a268896..5418448c9 100644 --- a/weed/mount/weedfs.go +++ b/weed/mount/weedfs.go @@ -60,6 +60,11 @@ type Option struct { DisableXAttr bool IsMacOs bool + // MaxInodeEntries bounds what readdirplus adds, not the table itself: only + // a kernel FORGET releases an entry, and LOOKUP is never refused. + // 0 is unlimited. + MaxInodeEntries int + // LogicalDiskUsage reports data sizes rather than the space they occupy, // for both df and the quota. See WFS.diskSizes. LogicalDiskUsage bool diff --git a/weed/mount/weedfs_dir_read.go b/weed/mount/weedfs_dir_read.go index e5493001b..73dbe66e9 100644 --- a/weed/mount/weedfs_dir_read.go +++ b/weed/mount/weedfs_dir_read.go @@ -214,6 +214,13 @@ func (wfs *WFS) doReadDirectory(input *fuse.ReadIn, out DirEntrySink, isPlusMode // Only a reference makes a child worth entering in the inode table: without // one nothing ever arrives to take the entry back out again. takesLookupRef := isPlusMode && out.TakesLookupRef() + // A zeroed EntryOut reads to the kernel as "no attributes for this entry": + // the name is still listed, but no reference is taken and none is owed back. + atInodeCap := takesLookupRef && wfs.option.MaxInodeEntries > 0 && + wfs.inodeToPath.Len() >= wfs.option.MaxInodeEntries + if atInodeCap { + takesLookupRef = false + } // index is the position in entryStream, used to calculate the offset for next readdir processEachEntryFn := func(entry *filer.Entry, index int64) bool { @@ -240,6 +247,9 @@ func (wfs *WFS) doReadDirectory(input *fuse.ReadIn, out DirEntrySink, isPlusMode if entryOut == nil { return false } + if atInodeCap { + return true + } if fh, found := wfs.fhMap.FindFileHandle(inode); found { glog.V(4).Infof("readdir opened file %s", childPath) entry = filer.FromPbEntry(string(dirPath), fh.GetEntry().GetEntry()) diff --git a/weed/mount/weedfs_dir_read_bench_test.go b/weed/mount/weedfs_dir_read_bench_test.go index a78da820d..e051560ed 100644 --- a/weed/mount/weedfs_dir_read_bench_test.go +++ b/weed/mount/weedfs_dir_read_bench_test.go @@ -62,12 +62,6 @@ func (s *benchSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut { func (s *benchSink) TakesLookupRef() bool { return s.takesRef } -func inodeTableSize(i *InodeToPath) int { - i.RLock() - defer i.RUnlock() - return len(i.inode2path) -} - func newBenchWFS(tb testing.TB, dir util.FullPath, n int) *WFS { tb.Helper() @@ -227,7 +221,7 @@ func BenchmarkReadDirectory(b *testing.B) { b.StopTimer() // What the listing left in the inode table, over the root and the // directory itself. - b.ReportMetric(float64(inodeTableSize(wfs.inodeToPath)), "inodes_left") + b.ReportMetric(float64(wfs.inodeToPath.Len()), "inodes_left") }) } } diff --git a/weed/mount/weedfs_dir_read_inode_cap_test.go b/weed/mount/weedfs_dir_read_inode_cap_test.go new file mode 100644 index 000000000..87e2f2a65 --- /dev/null +++ b/weed/mount/weedfs_dir_read_inode_cap_test.go @@ -0,0 +1,50 @@ +package mount + +import ( + "fmt" + "testing" + + "github.com/seaweedfs/go-fuse/v2/fuse" + "github.com/seaweedfs/seaweedfs/weed/util" +) + +// Past the cap the listing is still complete, but takes no more references. +func TestReadDirPlusStopsAtInodeCap(t *testing.T) { + dir := util.FullPath("/images") + names := make([]string, 40) + for i := range names { + names[i] = fmt.Sprintf("f%03d.jpg", i) + } + wfs := newPagingWFS(t, dir, names, 0) + wfs.option.MaxInodeEntries = 10 + dirInode, _ := wfs.inodeToPath.GetInode(dir) + + sink := &benchSink{plus: true, takesRef: true, sinkLimit: 8} + if got := walkOnce(t, wfs, dirInode, sink, false); got != len(names)+2 { + t.Fatalf("listed %d entries, want %d", got, len(names)+2) + } + if got := wfs.inodeToPath.Len(); got >= len(names) { + t.Fatalf("inode table holds %d, want it capped well under %d", got, len(names)) + } + + // Starting at the cap, nothing gets attributes. + dhid, _ := wfs.AcquireDirectoryHandle() + defer wfs.ReleaseDirectoryHandle(dhid) + sink.reset() + status := wfs.doReadDirectory(&fuse.ReadIn{ + InHeader: fuse.InHeader{NodeId: dirInode}, + Fh: uint64(dhid), + Size: 1 << 20, + }, sink, true) + if status != fuse.OK { + t.Fatalf("readdir: %v", status) + } + if sink.count == 0 { + t.Fatal("listed nothing") + } + for i := range sink.attrs { + if sink.attrs[i].NodeId != 0 { + t.Fatalf("entry %d took a reference past the cap", i) + } + } +}