fix(mount): bound reader cache memory across open files (#11220)

* fix(filer): bound retained reader cache buffers by bytes

* test(filer): keep in-flight downloads during cache trimming

* feat(mem): expose pooled allocation capacity for byte reservations

* fix(mount): share a configurable reader buffer budget across files

* fix(filer): release failed prefetch slots and memory reservations

* feat(mount): expose a soft Go runtime memory limit

* docs(filer): restore shared-download rationale in startCaching

The one-line comment replacing the original context.Background() explanation was too thin for readChunkAt to cross-reference shared resource semantics. Restore a concise note on why request cancellation must not abort a download shared by concurrent readers.

* test(filer): loosen reader cache test deadlines to 5s

Three tests used 1-second deadlines that can flake on CI under load:
TestReaderCacheBudgetInFlight, TestReaderCacheEvictionDoesNotHoldCacheLock,
and TestReaderCacheFailedPrefetchReleasesBudget. Increase to 5 seconds.

* test(filer): cover re-read after reader cache eviction

Add TestReaderCacheReReadAfterEviction: reads chunk 'a', reads chunk 'b'
(evicting 'a' via budget pressure), then re-reads 'a' and asserts a
fresh download returns correct data. Verifies the core correctness
property that eviction never exposes missing or stale data to readers.
This commit is contained in:
Chris Lu
2026-09-08 10:51:28 -07:00
committed by GitHub
parent c6b330be2b
commit 4a1d65939f
13 changed files with 497 additions and 31 deletions
+4
View File
@@ -20,6 +20,8 @@ type MountOptions struct {
chunkSizeLimitMB *int
concurrentWriters *int
concurrentReaders *int
readerCacheSizeMB *int64
memoryLimitMB *int64
cacheMetaTtlSec *int
cacheDirMaxEntries *int
cacheDirForRead *string
@@ -104,6 +106,8 @@ func init() {
mountOptions.chunkSizeLimitMB = cmdMount.Flag.Int("chunkSizeLimitMB", 2, "local write buffer size, also chunk large files")
mountOptions.concurrentWriters = cmdMount.Flag.Int("concurrentWriters", 128, "limit concurrent goroutine writers")
mountOptions.concurrentReaders = cmdMount.Flag.Int("concurrentReaders", 128, "limit concurrent chunk fetches for read operations")
mountOptions.memoryLimitMB = cmdMount.Flag.Int64("memoryLimitMB", 0, "soft Go runtime memory limit in MiB; 0 preserves GOMEMLIMIT; leave headroom below the container limit")
mountOptions.readerCacheSizeMB = cmdMount.Flag.Int64("readerCacheSizeMB", 256, "memory budget in MiB for downloaded and in-flight reader buffers across all files; must fit the largest pooled chunk buffer")
mountOptions.cacheDirForRead = cmdMount.Flag.String("cacheDir", os.TempDir(), "local cache directory for file chunks and meta data")
mountOptions.cacheSizeMBForRead = cmdMount.Flag.Int64("cacheCapacityMB", 128, "file chunk read cache capacity in MB")
mountOptions.cacheDirForWrite = cmdMount.Flag.String("cacheDirWrite", "", "buffer writes mostly for large files")
+22
View File
@@ -5,11 +5,13 @@ package command
import (
"context"
"fmt"
"math"
"net"
"net/http"
"os"
"path"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
@@ -184,6 +186,10 @@ type fileSystemParams struct {
}
func buildSeaweedFileSystem(option *MountOptions, p fileSystemParams) *mount.WFS {
readerCacheSizeMB := int64(256)
if option.readerCacheSizeMB != nil {
readerCacheSizeMB = *option.readerCacheSizeMB
}
return mount.NewSeaweedFileSystem(&mount.Option{
MountDirectory: p.dir,
FilerAddresses: p.filerAddresses,
@@ -198,6 +204,7 @@ func buildSeaweedFileSystem(option *MountOptions, p fileSystemParams) *mount.WFS
ChunkSizeLimit: int64(p.chunkSizeLimitMB) * 1024 * 1024,
ConcurrentWriters: *option.concurrentWriters,
ConcurrentReaders: *option.concurrentReaders,
ReaderCacheSizeMB: readerCacheSizeMB,
CacheDirForRead: p.cacheDirForRead,
CacheSizeMBForRead: *option.cacheSizeMBForRead,
CacheDirForWrite: p.cacheDirForWrite,
@@ -304,3 +311,18 @@ func lastSegment(p string) string {
}
return name
}
func configureMountMemory(option *MountOptions) error {
if option.readerCacheSizeMB != nil && (*option.readerCacheSizeMB <= 0 || *option.readerCacheSizeMB > math.MaxInt64>>20) {
return fmt.Errorf("readerCacheSizeMB must be positive and fit in an int64 byte budget")
}
if option.memoryLimitMB != nil {
if *option.memoryLimitMB < 0 || *option.memoryLimitMB > math.MaxInt64>>20 {
return fmt.Errorf("memoryLimitMB must be non-negative and fit in an int64 byte limit")
}
if *option.memoryLimitMB > 0 {
debug.SetMemoryLimit(*option.memoryLimitMB << 20)
}
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
//go:build linux || darwin || freebsd || windows
package command
import (
"math"
"runtime/debug"
"testing"
)
func TestConfigureMountMemory(t *testing.T) {
for _, size := range []int64{-1, 0, 1, 256, math.MaxInt64 >> 20, math.MaxInt64} {
err := configureMountMemory(&MountOptions{readerCacheSizeMB: &size})
valid := size > 0 && size <= math.MaxInt64>>20
if (err == nil) != valid {
t.Errorf("size=%d: err=%v", size, err)
}
}
}
func TestConfigureMountMemoryRuntimeLimit(t *testing.T) {
previous := debug.SetMemoryLimit(512 << 20)
defer debug.SetMemoryLimit(previous)
for _, size := range []int64{0, -1, math.MaxInt64, 768, 0} {
before := debug.SetMemoryLimit(-1)
err := configureMountMemory(&MountOptions{memoryLimitMB: &size})
valid := size >= 0 && size <= math.MaxInt64>>20
if (err == nil) != valid {
t.Errorf("size=%d: err=%v", size, err)
}
want := before
if valid && size > 0 {
want = size << 20
}
if got := debug.SetMemoryLimit(-1); got != want {
t.Errorf("size=%d: runtime limit=%d, want %d", size, got, want)
}
}
}
+4
View File
@@ -24,6 +24,10 @@ import (
)
func RunMount(option *MountOptions, umask os.FileMode) bool {
if err := configureMountMemory(option); err != nil {
fmt.Println(err)
return false
}
// basic checks
chunkSizeLimitMB := *mountOptions.chunkSizeLimitMB
+5
View File
@@ -30,6 +30,11 @@ const ownedByMounter = ^uint32(0)
const windowsCacheTimeout = time.Second
func RunMount(option *MountOptions, umask os.FileMode) bool {
if err := configureMountMemory(option); err != nil {
fmt.Println(err)
return false
}
chunkSizeLimitMB := *mountOptions.chunkSizeLimitMB
if chunkSizeLimitMB <= 0 {
fmt.Printf("Please specify a reasonable buffer size.\n")