Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0ff8a5c
feat(caching): implement negative stat cache to optimize polling of m…
alleaditya May 25, 2026
6ed7904
docs(fs): add detailed comment explaining LookUpChild short-circuit l…
alleaditya May 25, 2026
633c027
style: fix formatting in bucket_manager.go
alleaditya May 25, 2026
5afbc7f
fix(caching): address review comments and resolve data races/deadlock…
alleaditya May 25, 2026
3bf09de
style: remove extra blank line in fast_stat_bucket.go
alleaditya May 25, 2026
063c48a
refactor(caching): remove unrelated fixes and speculative listing cac…
alleaditya May 25, 2026
6cd627c
fix(caching): add negative caching for empty directories in fast_stat…
alleaditya Jun 4, 2026
85ef618
Revert changes to internal/fs/inode/dir.go as requested by reviewer
alleaditya Jun 4, 2026
7844a2d
fix(caching): remove speculative negative caching from insertListing
alleaditya Jun 8, 2026
a41f2f7
fix(caching): safely re-add negative caching to insertListing
alleaditya Jun 8, 2026
e4c4072
style: apply go fmt to tests
alleaditya Jun 8, 2026
b04bef2
chore: revert unintended formatting changes in hns_bucket_test.go
alleaditya Jun 8, 2026
0dfc7f6
style(caching): combine nested if conditions in insertListing
alleaditya Jun 8, 2026
855f4f0
fix(caching): move negative caching TTL check to caller
alleaditya Jun 8, 2026
0370a6a
refactor(caching): clean up boilerplate if-else blocks by using a hel…
alleaditya Jun 8, 2026
e70f114
Fix cache bug and combine condition
alleaditya Jun 8, 2026
ef534ee
Add b.implicitDir check to negative cache insertion condition
alleaditya Jun 10, 2026
abf86a8
Address PR comment: Use newline to separate AAA in gcs_metrics_test
alleaditya Jun 11, 2026
b928d18
Merge remote-tracking branch 'upstream/master' into add-negative-stat…
alleaditya Jun 11, 2026
f7e0e0c
test(metrics): use t.TempDir() instead of hardcoded tmp directory in …
alleaditya Jun 11, 2026
1311bb8
test(metrics): update short-circuit test to simulate list call with -…
alleaditya Jun 11, 2026
21c24f0
test(metrics): clean up comments in TestGCSMetrics_RequestCount_Negat…
alleaditya Jun 12, 2026
955c914
Update internal/fs/gcs_metrics_test.go
alleaditya Jun 15, 2026
e5a2c07
refactor(metrics): remove unused type definition in gcs_metrics_test
alleaditya Jun 15, 2026
5abeb0b
Short-circuit ListObjects if negative cache is hit
alleaditya Jun 16, 2026
0e32dbe
test(metrics): advance cacheClock for cross-mount tests
alleaditya Jun 16, 2026
5c2ff9f
fix(cache): restrict implicit dir negative caching and inference to d…
alleaditya Jun 16, 2026
fd86253
style(cache): apply De Morgan's law to satisfy staticcheck QF1001
alleaditya Jun 16, 2026
b7e0bdc
Fix staticcheck QF1001 and allow short-circuit in ListObjects for exp…
alleaditya Jun 16, 2026
fc4f668
Revert "allow short-circuit in ListObjects for explicit-dirs and HNS"
alleaditya Jun 16, 2026
006f3d9
fix(cache): preserve folder in LRU and fix ListObjects short-circuit …
alleaditya Jun 17, 2026
74843d4
Revert "fix(cache): preserve folder in LRU and fix ListObjects short-…
alleaditya Jun 17, 2026
ceb9bb1
fix(cache): fix ListObjects short-circuit for HNS folders
alleaditya Jun 17, 2026
51e42bc
Merge remote-tracking branch 'upstream/master' into add-negative-stat…
alleaditya Jun 17, 2026
c2fae1f
Fix integration test cache issues
alleaditya Jun 18, 2026
76b9fad
Address PR comments: Move negative cache to implicit directories sect…
alleaditya Jun 18, 2026
071d269
Refactor ListObjects negative cache short-circuiting logic
alleaditya Jun 18, 2026
891c487
Fix build error: pass time.Time to statCache LookUp
alleaditya Jun 18, 2026
bf412c2
Simplify ListObjects short-circuiting by removing unreachable HNS buc…
alleaditya Jun 18, 2026
c8ab55a
Revert addition of --metadata-cache-negative-ttl-secs=2 in dentry_cac…
alleaditya Jun 18, 2026
beab398
test: use unique t.TempDir() in file_cache_reader_test to fix cross-s…
alleaditya Jun 18, 2026
27d4998
test: restore --metadata-cache-negative-ttl-secs=2 to dentry_cache tests
alleaditya Jun 18, 2026
e0da02e
test: restore --metadata-cache-negative-ttl-secs=2 to test_config.yaml
alleaditya Jun 19, 2026
b9f8514
Fix integration test failures and restore flags
alleaditya Jun 19, 2026
9b1dd6d
test: Add negative TTL flag to CI integration test config
alleaditya Jun 19, 2026
f430837
Fix dentry_cache test failures due to implicit directory caching
alleaditya Jun 19, 2026
109b209
Revert addition of --metadata-cache-negative-ttl-secs in test_config.…
alleaditya Jun 19, 2026
10e5e13
Revert test changes as requested
alleaditya Jun 19, 2026
d8b9371
Remove negative caching of empty directories in insertListing
alleaditya Jun 19, 2026
6b0387e
Run go fmt
alleaditya Jun 19, 2026
ac90e84
Fix file_cache_reader_test crosstalk
alleaditya Jun 19, 2026
abf58ba
Merge branch 'master' into add-negative-stat-cache
alleaditya Jun 19, 2026
0bc9494
Restore negative stat cache for empty directories
alleaditya Jun 19, 2026
09de3a3
Fix dentry_cache integration tests by clearing test cache
alleaditya Jun 19, 2026
7c18968
Fix dentry_cache integration tests in test_config.yaml
alleaditya Jun 20, 2026
506cb06
Revert unnecessary destroy call
alleaditya Jun 20, 2026
b62cc6b
Fix redundant ListObjects calls by negatively caching missing directo…
alleaditya Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,12 @@ The behavior of stat cache is controlled by the following flags/config parameter
If this config variable is missing, then the value of ```--stat-cache-ttl``` is used.
* ```--stat-cache-ttl``` commandline flag, which can be set to a value like ```10s``` or ```1.5h```. The default is one minute. This has been deprecated (starting v2.0) and is currently only available for backward compatibility. If ```metadata-cache: ttl-secs``` is set, ```--stat-cache-ttl``` is ignored.

Positive and negative stat results will be cached for the specified amount of time.
Positive stat results (existing files) will be cached for this specified amount of time.

3. **Negative Stat-cache (Non-existent Entry Caching)**: Controls caching of non-existent paths (`ErrNotExist` / 404 results) to optimize workloads that aggressively poll missing files (e.g., JupyterLab).
* `metadata-cache: negative-ttl-secs` in the config-file (integer). Sets the TTL in seconds for negative entries. Default is 5 seconds. Use -1 for infinite TTL. Setting this to 0 disables negative caching.
Comment thread
alleaditya marked this conversation as resolved.
> [!WARNING]
> Using an infinite TTL (`-1`) for negative caching is risky if objects are created externally (by other clients or processes), as those new files/directories will never be visible under the mount until the cache is manually cleared or GCSFuse is restarted.

Warnings:
- Using stat caching breaks the consistency guarantees discussed in this document. It is safe only in the following situations:
Expand Down
84 changes: 84 additions & 0 deletions internal/fs/gcs_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import (
"os"
"testing"

"time"

"github.com/googlecloudplatform/gcsfuse/v3/cfg"
"github.com/googlecloudplatform/gcsfuse/v3/internal/cache/lru"
"github.com/googlecloudplatform/gcsfuse/v3/internal/cache/metadata"
"github.com/googlecloudplatform/gcsfuse/v3/internal/fs"
"github.com/googlecloudplatform/gcsfuse/v3/internal/fs/wrappers"
"github.com/googlecloudplatform/gcsfuse/v3/internal/gcsx"
"github.com/googlecloudplatform/gcsfuse/v3/internal/monitor"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/caching"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/fake"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/gcs"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/storageutil"
Expand Down Expand Up @@ -463,3 +468,82 @@ func TestGCSMetrics_RetryCount(t *testing.T) {
attribute.NewSet(attribute.String("retry_error_category", "STALLED_READ_REQUEST")),
1)
}

type fakeBucketManagerForShortCircuit struct {
bucket gcs.Bucket
}

func (bm *fakeBucketManagerForShortCircuit) SetUpBucket(ctx context.Context, name string, _ bool, _ metrics.MetricHandle) (gcsx.SyncerBucket, error) {
return gcsx.NewSyncerBucket(0, 120, 10, ".gcsfuse_tmp/", gcsx.NewContentTypeBucket(bm.bucket)), nil
}
func (bm *fakeBucketManagerForShortCircuit) ShutDown() {}
Comment thread
alleaditya marked this conversation as resolved.

// TestGCSMetrics_RequestCount_NegativeCachingShortCircuit validates that when negative entry caching is enabled,
// repeated lookups for non-existent files short-circuit in memory and do not emit redundant backend GCS requests.
func TestGCSMetrics_RequestCount_NegativeCachingShortCircuit(t *testing.T) {
Comment thread
alleaditya marked this conversation as resolved.
ctx := context.Background()
origProvider := otel.GetMeterProvider()
t.Cleanup(func() { otel.SetMeterProvider(origProvider) })
reader := metric.NewManualReader()
provider := metric.NewMeterProvider(metric.WithReader(reader))
otel.SetMeterProvider(provider)

mh, err := metrics.NewOTelMetrics(ctx, 1, 100)
require.NoError(t, err, "metrics.NewOTelMetrics")

bucketName := "test-bucket"
rawBucket := fake.NewFakeBucket(timeutil.RealClock(), bucketName, gcs.BucketType{Hierarchical: false})

// Enforce production wrapping order: Caching layer MUST wrap Monitoring layer.
monitoringInnerBucket := monitor.NewMonitoringBucket(rawBucket, mh)
lruCache := lru.NewCache(1024 * 1024)
statCacheView := metadata.NewStatCacheBucketView(lruCache, "")
cachedOuterBucket := caching.NewFastStatBucket(
time.Minute,
statCacheView,
timeutil.RealClock(),
monitoringInnerBucket,
time.Minute,
true, // isTypeCacheDeprecated
true, // implicitDir
)

serverCfg := &fs.ServerConfig{
NewConfig: &cfg.Config{
EnableNewReader: true,
},
MetricHandle: mh,
TraceHandle: tracing.NewNoopTracer(),
CacheClock: &timeutil.SimulatedClock{},
BucketName: bucketName,
BucketManager: &fakeBucketManagerForShortCircuit{
bucket: cachedOuterBucket,
},
Comment thread
alleaditya marked this conversation as resolved.
}

server, err := fs.NewFileSystem(ctx, serverCfg)
require.NoError(t, err, "NewFileSystem")

lookupOp := &fuseops.LookUpInodeOp{
Parent: fuseops.RootInodeID,
Name: "missing_file",
}

// 1. First Lookup (Cache Miss) -> Emits backend probes through monitoring layer
_ = server.LookUpInode(ctx, lookupOp)
waitForMetricsProcessing()

// Probing a missing file checks dir then file -> 2 actual backend network calls.
metrics.VerifyCounterMetric(t, ctx, reader, "gcs/request_count",
Comment thread
alleaditya marked this conversation as resolved.
attribute.NewSet(attribute.String("gcs_method", "StatObject")),
2)

// 2. Second Lookup (Negative Cache Hit) -> Intercepted by outer cache layer
_ = server.LookUpInode(ctx, lookupOp)
waitForMetricsProcessing()

// Verify backend request count remains exactly 2 (zero new network requests emitted)
metrics.VerifyCounterMetric(t, ctx, reader, "gcs/request_count",
attribute.NewSet(attribute.String("gcs_method", "StatObject")),
2)
}
3 changes: 3 additions & 0 deletions internal/storage/caching/fast_stat_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ func (b *fastStatBucket) insertListing(ctx context.Context, listing *gcs.Listing
if b.implicitDir && dirHasContents && !isDirInListing {
b.cache.InsertImplicitDir(dirName, expiration)
}
if b.implicitDir && !dirHasContents && dirName != "" && b.negativeCacheTTL > 0 {
b.cache.AddNegativeEntry(dirName, b.clock.Now().Add(b.negativeCacheTTL))
}
Comment thread
alleaditya marked this conversation as resolved.
Outdated

// 2. Cache Explicit Objects
for _, o := range listing.MinObjects {
Expand Down
3 changes: 3 additions & 0 deletions internal/storage/caching/fast_stat_bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,9 @@ func (t *ListObjectsTest_InsertListing) callAndVerify(ctx context.Context, isHNS
for _, dir := range expectedImplicitDirs {
ExpectCall(t.cache, "InsertImplicitDir")(dir, Any())
}
if len(listing.MinObjects) == 0 && len(listing.CollapsedRuns) == 0 && prefix != "" {
ExpectCall(t.cache, "AddNegativeEntry")(prefix, Any())
}

// Call
gotListing, err := t.bucket.ListObjects(ctx, &gcs.ListObjectsRequest{Prefix: prefix})
Expand Down
7 changes: 7 additions & 0 deletions internal/storage/fake/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"time"
"unicode/utf8"

"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/caching"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/gcs"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/storageutil"
"github.com/jacobsa/syncutil"
Expand Down Expand Up @@ -900,6 +901,9 @@ func (b *bucket) ComposeObjects(
// LOCKS_EXCLUDED(b.mu)
func (b *bucket) StatObject(ctx context.Context,
req *gcs.StatObjectRequest) (m *gcs.MinObject, e *gcs.ExtendedObjectAttributes, err error) {
if req.FetchOnlyFromCache {
return nil, nil, &caching.CacheMissError{Err: errors.New("fake bucket has no cache")}
}
// If ExtendedObjectAttributes are requested without fetching from gcs enabled, panic.
if !req.ForceFetchFromGcs && req.ReturnExtendedObjectAttributes {
panic("invalid StatObjectRequest: ForceFetchFromGcs: false and ReturnExtendedObjectAttributes: true")
Expand Down Expand Up @@ -1143,6 +1147,9 @@ func (b *bucket) DeleteFolder(ctx context.Context, folderName string) (err error
}

func (b *bucket) GetFolder(ctx context.Context, req *gcs.GetFolderRequest) (*gcs.Folder, error) {
if req.FetchOnlyFromCache {
return nil, &caching.CacheMissError{Err: errors.New("fake bucket has no cache")}
}
b.mu.Lock()
defer b.mu.Unlock()

Expand Down
Loading