From 1b73b312514a8ce07cb5762d434eb5a34de32ac2 Mon Sep 17 00:00:00 2001 From: Alexandre Manhaes Savio Date: Tue, 18 Aug 2026 12:55:01 +0200 Subject: [PATCH 1/2] fix(symlinks): re-arm the cache TTL when the reload returns 304 loadSymlinksCache returned early on a 304 without touching symlinksCacheTime, so the TTL guard at the top of the function never passed again. In any directory that has a .geesefs_symlinks file, every failed lookup past the first TTL expiry re-issued the conditional GET. Failed lookups are the most common metadata operation on a mount (import path search, stat probes, shell completion), and --enable-symlinks-file is passed on every session and job VM, so this put an S3 round trip on the hottest metadata path. A 304 means the cached copy is current, so record the check time and let the guard serve the next miss locally. --- core/dir.go | 5 ++++- core/symlinks_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/dir.go b/core/dir.go index 8df75705..ec541e78 100644 --- a/core/dir.go +++ b/core/dir.go @@ -1876,9 +1876,12 @@ func (parent *Inode) loadSymlinksCache() error { return err } - // If data is nil, the file hasn't changed (304 Not Modified) + // If data is nil, the file hasn't changed (304 Not Modified). The cached copy + // is still current, so re-arm the TTL: without this the guard above never + // passes again and every later miss re-issues the conditional GET. if data == nil { s3Log.Debugf("loadSymlinksCache: file unchanged (304), dir=%v", parent.FullName()) + parent.dir.symlinksCacheTime = time.Now() return nil } diff --git a/core/symlinks_test.go b/core/symlinks_test.go index 725dc578..e9080194 100644 --- a/core/symlinks_test.go +++ b/core/symlinks_test.go @@ -1297,3 +1297,38 @@ func (s *SymlinksTest) TestBatchRemoveOnlySymlink(t *C) { parent.mu.Unlock() } +func (s *SymlinksTest) TestCacheNotModifiedRearmsTTL(t *C) { + mock := newMockConditionalBackend() + _, parent := newTestDirInode(mock, 1*time.Hour) + + // Give the directory a symlinks file so the reload can come back 304. + data := NewSymlinksFileData() + data.AddSymlink("link1", "../target1") + _, err := SaveSymlinksFile(mock, "", ".geesefs_symlinks", data, "") + t.Assert(err, IsNil) + + gets := 0 + mock.onGetBlob = func(param *GetBlobInput) { gets++ } + + parent.mu.Lock() + defer parent.mu.Unlock() + + // First load populates the cache and records the ETag. + err = parent.loadSymlinksCache() + t.Assert(err, IsNil) + t.Assert(gets, Equals, 1) + t.Assert(parent.dir.symlinksCache.HasSymlink("link1"), Equals, true) + + // Expire the cache; the reload sends If-None-Match and gets a 304 back. + parent.dir.symlinksCacheTime = time.Time{} + err = parent.loadSymlinksCache() + t.Assert(err, IsNil) + t.Assert(gets, Equals, 2) + + // The 304 confirmed the cache is current, so the next miss within the TTL + // must be served locally instead of re-issuing the conditional GET. + err = parent.loadSymlinksCache() + t.Assert(err, IsNil) + t.Assert(gets, Equals, 2) + t.Assert(parent.dir.symlinksCache.HasSymlink("link1"), Equals, true) +} From 4117ec95d64ff77adc9f5f48928189c0f9a7321f Mon Sep 17 00:00:00 2001 From: Alexandre Manhaes Savio Date: Tue, 18 Aug 2026 13:06:48 +0200 Subject: [PATCH 2/2] chore: release 0.43.8-dc.3 --- core/cfg/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/cfg/flags.go b/core/cfg/flags.go index 944e227e..a861a5e3 100644 --- a/core/cfg/flags.go +++ b/core/cfg/flags.go @@ -30,7 +30,7 @@ import ( "github.com/urfave/cli" ) -const GEESEFS_VERSION = "0.43.8-dc.2" +const GEESEFS_VERSION = "0.43.8-dc.3" var flagCategories map[string]string