diff --git a/internal/cache/metadata/stat_cache.go b/internal/cache/metadata/stat_cache.go index dfcddfa7ec5..aba86fc1826 100644 --- a/internal/cache/metadata/stat_cache.go +++ b/internal/cache/metadata/stat_cache.go @@ -113,7 +113,7 @@ type statCacheBucketView struct { type entry struct { m *gcs.MinObject f *gcs.Folder - expiration time.Time + expiration int64 // expiration timestamp // Set to true only for implicit directory entries. This flag will always remain false for negative entries and explicit objects. implicitDir bool } @@ -145,6 +145,28 @@ func (e entry) Size() uint64 { return size } +// timeToUnixNano safely converts a time.Time to Unix nanoseconds. +// It prevents int64 overflow for dates beyond the year 2262, returning +// math.MaxInt64 instead, which safely mimics "never expire". +func timeToUnixNano(t time.Time) int64 { + if t.IsZero() { + return math.MinInt64 + } + + // 9223372036 is math.MaxInt64 / 1,000,000,000 (max seconds representable in nanoseconds) + const maxSeconds int64 = math.MaxInt64 / 1000000000 + + unixSecs := t.Unix() + if unixSecs > maxSeconds { + return math.MaxInt64 + } + if unixSecs < -maxSeconds { + return math.MinInt64 + } + + return t.UnixNano() +} + // Should the supplied object for a new positive entry replace the given // existing entry? func shouldReplace(m *gcs.MinObject, existing entry) bool { @@ -201,7 +223,7 @@ func (sc *statCacheBucketView) Insert(m *gcs.MinObject, expiration time.Time) { // Insert an entry. e := entry{ m: m, - expiration: expiration, + expiration: timeToUnixNano(expiration), } if _, err := sc.sharedCache.Insert(name, e); err != nil { @@ -237,7 +259,7 @@ func (sc *statCacheBucketView) InsertImplicitDir(objectName string, expiration t // Insert an entry. e := entry{ implicitDir: true, - expiration: expiration, + expiration: timeToUnixNano(expiration), } if _, err := sc.sharedCache.Insert(name, e); err != nil { @@ -251,7 +273,7 @@ func (sc *statCacheBucketView) AddNegativeEntry(objectName string, expiration ti // Insert a negative entry. e := entry{ m: nil, - expiration: expiration, + expiration: timeToUnixNano(expiration), } if _, err := sc.sharedCache.Insert(name, e); err != nil { @@ -265,7 +287,7 @@ func (sc *statCacheBucketView) AddNegativeEntryForFolder(folderName string, expi // Insert a negative entry. e := entry{ f: nil, - expiration: expiration, + expiration: timeToUnixNano(expiration), } if _, err := sc.sharedCache.Insert(name, e); err != nil { @@ -315,7 +337,7 @@ func (sc *statCacheBucketView) sharedCacheLookup(key string, now time.Time) (boo e := value.(entry) // Has this entry expired? - if e.expiration.Before(now) { + if e.expiration < timeToUnixNano(now) { sc.Erase(key) return false, nil } @@ -328,7 +350,7 @@ func (sc *statCacheBucketView) InsertFolder(f *gcs.Folder, expiration time.Time) e := entry{ f: f, - expiration: expiration, + expiration: timeToUnixNano(expiration), } if _, err := sc.sharedCache.Insert(name, e); err != nil { diff --git a/internal/cache/metadata/stat_cache_benchmark_test.go b/internal/cache/metadata/stat_cache_benchmark_test.go new file mode 100644 index 00000000000..bf9157faf7a --- /dev/null +++ b/internal/cache/metadata/stat_cache_benchmark_test.go @@ -0,0 +1,85 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metadata_test + +import ( + "fmt" + "testing" + "time" + + "github.com/googlecloudplatform/gcsfuse/v3/internal/cache/lru" + "github.com/googlecloudplatform/gcsfuse/v3/internal/cache/metadata" + "github.com/googlecloudplatform/gcsfuse/v3/internal/storage/gcs" +) + +func generateKeys(count int) []string { + keys := make([]string, count) + for i := 0; i < count; i++ { + keys[i] = fmt.Sprintf("object-%d", i) + } + return keys +} + +func setupStatCache(capacity uint64) metadata.StatCache { + // 1000 items * approx 120 bytes each = ~120,000 bytes capacity + lruCache := lru.NewCache(capacity) + return metadata.NewStatCacheBucketView(lruCache, "") +} + +func BenchmarkStatCache_Insert(b *testing.B) { + keys := generateKeys(10000) + cache := setupStatCache(uint64(len(keys) * 500)) + now := time.Now() + + i := 0 + for b.Loop() { + key := keys[i%len(keys)] + m := &gcs.MinObject{Name: key} + cache.Insert(m, now) + i++ + } +} + +func BenchmarkStatCache_AddNegativeEntry(b *testing.B) { + keys := generateKeys(10000) + cache := setupStatCache(uint64(len(keys) * 500)) + now := time.Now() + + i := 0 + for b.Loop() { + key := keys[i%len(keys)] + cache.AddNegativeEntry(key, now) + i++ + } +} + +func BenchmarkStatCache_LookUp(b *testing.B) { + keys := generateKeys(10000) + cache := setupStatCache(uint64(len(keys) * 500)) + now := time.Now() + + // Pre-fill the cache with unexpired entries. + for _, key := range keys { + m := &gcs.MinObject{Name: key} + cache.Insert(m, now.Add(time.Hour)) + } + + i := 0 + for b.Loop() { + key := keys[i%len(keys)] + _, _ = cache.LookUp(key, now) + i++ + } +}