Skip to content
36 changes: 29 additions & 7 deletions internal/cache/metadata/stat_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ type statCacheBucketView struct {
type entry struct {
m *gcs.MinObject
f *gcs.Folder
expiration time.Time
expiration int64 // expiration timestamp
Comment thread
vedantdas-source marked this conversation as resolved.
// Set to true only for implicit directory entries. This flag will always remain false for negative entries and explicit objects.
implicitDir bool
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
}
Comment thread
vedantdas-source marked this conversation as resolved.

if _, err := sc.sharedCache.Insert(name, e); err != nil {
Expand Down Expand Up @@ -237,7 +259,7 @@ func (sc *statCacheBucketView) InsertImplicitDir(objectName string, expiration t
// Insert an entry.
e := entry{
implicitDir: true,
expiration: expiration,
expiration: timeToUnixNano(expiration),
}
Comment thread
vedantdas-source marked this conversation as resolved.

if _, err := sc.sharedCache.Insert(name, e); err != nil {
Expand All @@ -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),
}
Comment thread
vedantdas-source marked this conversation as resolved.

if _, err := sc.sharedCache.Insert(name, e); err != nil {
Expand All @@ -265,7 +287,7 @@ func (sc *statCacheBucketView) AddNegativeEntryForFolder(folderName string, expi
// Insert a negative entry.
e := entry{
f: nil,
expiration: expiration,
expiration: timeToUnixNano(expiration),
}
Comment thread
vedantdas-source marked this conversation as resolved.

if _, err := sc.sharedCache.Insert(name, e); err != nil {
Expand Down Expand Up @@ -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
}
Comment thread
vedantdas-source marked this conversation as resolved.
Expand All @@ -328,7 +350,7 @@ func (sc *statCacheBucketView) InsertFolder(f *gcs.Folder, expiration time.Time)

e := entry{
f: f,
expiration: expiration,
expiration: timeToUnixNano(expiration),
}

Comment thread
vedantdas-source marked this conversation as resolved.
if _, err := sc.sharedCache.Insert(name, e); err != nil {
Expand Down
85 changes: 85 additions & 0 deletions internal/cache/metadata/stat_cache_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -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++
}
}
Loading