Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions cmd/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ be interacting with the file system.`)
FinalizeFileForRapid: newConfig.Write.FinalizeFileForRapid,
DisableListAccessCheck: newConfig.DisableListAccessCheck,
DummyIOCfg: newConfig.DummyIo,
IsTypeCacheDeprecated: newConfig.EnableTypeCacheDeprecation,
}
bm := gcsx.NewBucketManager(bucketCfg, storageHandle)

Expand Down
146 changes: 102 additions & 44 deletions internal/fs/inode/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/googlecloudplatform/gcsfuse/v3/internal/gcsx"
"github.com/googlecloudplatform/gcsfuse/v3/internal/locker"
"github.com/googlecloudplatform/gcsfuse/v3/internal/logger"
"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/fuse/fuseops"
Expand Down Expand Up @@ -251,6 +252,8 @@ type dirInode struct {
// Represents if folder has been unlinked in hierarchical bucket. This is not getting used in
// non-hierarchical bucket.
unlinked bool

metadataCacheTtlSecs int64
}

var _ DirInode = &dirInode{}
Expand Down Expand Up @@ -306,6 +309,7 @@ func NewDirInode(
isUnsupportedPathSupportEnabled: cfg.EnableUnsupportedPathSupport,
isEnableTypeCacheDeprecation: cfg.EnableTypeCacheDeprecation,
unlinked: false,
metadataCacheTtlSecs: cfg.MetadataCache.TtlSecs,
}
// readObjectsUnlocked is used by the prefetcher so the background worker performs GCS I/O without the lock,
// acquiring d.mu only to update the cache.
Expand Down Expand Up @@ -338,19 +342,19 @@ func (d *dirInode) checkInvariants() {
}

func (d *dirInode) lookUpChildFile(ctx context.Context, name string) (*Core, error) {
return findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name))
return findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name), false)
}

func (d *dirInode) lookUpChildDir(ctx context.Context, name string) (*Core, error) {
childName := NewDirName(d.Name(), name)
if d.isBucketHierarchical() {
return findExplicitFolder(ctx, d.Bucket(), childName)
return findExplicitFolder(ctx, d.Bucket(), childName, false)
}

if d.implicitDirs {
return findDirInode(ctx, d.Bucket(), childName)
return findDirInode(ctx, d.Bucket(), childName, d.isEnableTypeCacheDeprecation)
}
return findExplicitInode(ctx, d.Bucket(), childName)
return findExplicitInode(ctx, d.Bucket(), childName, false)
}

// Look up the file for a (file, dir) pair with conflicting names, overriding
Expand Down Expand Up @@ -384,10 +388,11 @@ func (d *dirInode) lookUpConflicting(ctx context.Context, name string) (*Core, e

// findExplicitInode finds the file or dir inode core backed by an explicit
// object in GCS with the given name. Return nil if such object does not exist.
func findExplicitInode(ctx context.Context, bucket *gcsx.SyncerBucket, name Name) (*Core, error) {
func findExplicitInode(ctx context.Context, bucket *gcsx.SyncerBucket, name Name, fetchOnlyFromCache bool) (*Core, error) {
// Call the bucket.
req := &gcs.StatObjectRequest{
Name: name.GcsObjectName(),
Name: name.GcsObjectName(),
FetchOnlyFromCache: fetchOnlyFromCache,
}

m, _, err := bucket.StatObject(ctx, req)
Expand All @@ -403,17 +408,25 @@ func findExplicitInode(ctx context.Context, bucket *gcsx.SyncerBucket, name Name
return nil, fmt.Errorf("StatObject: %w", err)
}

// Treat this as an implicit directory. Since implicit objects lack metadata
// (only the name is preserved), we set MinObject to nil. This ensures the
// inode is correctly assigned the ImplicitDir Core Type.
if fetchOnlyFromCache && m.Generation == 0 {
m = nil
}

return &Core{
Bucket: bucket,
FullName: name,
MinObject: m,
}, nil
}

func findExplicitFolder(ctx context.Context, bucket *gcsx.SyncerBucket, name Name) (*Core, error) {
func findExplicitFolder(ctx context.Context, bucket *gcsx.SyncerBucket, name Name, fetchOnlyFromCache bool) (*Core, error) {
// Call the bucket.
req := &gcs.GetFolderRequest{
Name: name.GcsObjectName(),
Name: name.GcsObjectName(),
FetchOnlyFromCache: fetchOnlyFromCache,
}
folder, err := bucket.GetFolder(ctx, req)

Expand All @@ -437,14 +450,15 @@ func findExplicitFolder(ctx context.Context, bucket *gcsx.SyncerBucket, name Nam

// findDirInode finds the dir inode core where the directory is either explicit
// or implicit. Returns nil if no such directory exists.
func findDirInode(ctx context.Context, bucket *gcsx.SyncerBucket, name Name) (*Core, error) {
func findDirInode(ctx context.Context, bucket *gcsx.SyncerBucket, name Name, isTypeCacheDeprecated bool) (*Core, error) {
if !name.IsDir() {
return nil, fmt.Errorf("%q is not directory", name)
}

req := &gcs.ListObjectsRequest{
Prefix: name.GcsObjectName(),
MaxResults: 1,
Prefix: name.GcsObjectName(),
MaxResults: 1,
IsTypeCacheDeprecated: isTypeCacheDeprecated,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is typeCacheDeprecated flag being passed as part of listObjects request>?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is to enable new caching logic in fast stat bucket
ref:

if req.IsTypeCacheDeprecated {

}
listing, err := bucket.ListObjects(ctx, req)
if err != nil {
Expand Down Expand Up @@ -583,40 +597,93 @@ func (d *dirInode) LookUpChild(ctx context.Context, name string) (*Core, error)
return d.lookUpConflicting(ctx, name)
}

cachedType := metadata.UnknownType

// 1. Optimization: If Type Cache is deprecated, attempt a lookup via the Stat Cache first.
// We skip this if the metadata cache TTL is 0, as the cache layer is inactive.
if d.IsTypeCacheDeprecated() && d.metadataCacheTtlSecs != 0 {
var cacheMissErr *caching.CacheMissError

// Try File
fileResult, err := findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name), true)
if err != nil && !errors.As(err, &cacheMissErr) {
return nil, err
}

// Try Directory
var dirResult *Core
if d.Bucket().BucketType().Hierarchical {
dirResult, err = findExplicitFolder(ctx, d.Bucket(), NewDirName(d.Name(), name), true)
} else {
dirResult, err = findExplicitInode(ctx, d.Bucket(), NewDirName(d.Name(), name), true)
}
if err != nil && !errors.As(err, &cacheMissErr) {
return nil, err
}

if dirResult != nil {
return dirResult, nil
} else if fileResult != nil {
return fileResult, nil
}
}

// 2. Legacy: If Type Cache is NOT deprecated, fetch the type hint.
if !d.IsTypeCacheDeprecated() {
cachedType = d.cache.Get(d.cacheClock.Now(), name)
}

// 3. Main Lookup Logic (Unified)
result, err := d.fetchCoreEntity(ctx, name, cachedType)
if err != nil {
return nil, err
}

// 4. Legacy: Update Type Cache if needed.
if !d.IsTypeCacheDeprecated() {
if result != nil {
d.cache.Insert(d.cacheClock.Now(), name, result.Type())
} else if d.enableNonexistentTypeCache && cachedType == metadata.UnknownType {
d.cache.Insert(d.cacheClock.Now(), name, metadata.NonexistentType)
}
}

return result, nil
}

// fetchCoreEntity contains all the existing logic for looking up children
// without worrying about the isTypeCacheDeprecated flag.
func (d *dirInode) fetchCoreEntity(ctx context.Context, name string, cachedType metadata.Type) (*Core, error) {
group, ctx := errgroup.WithContext(ctx)

var fileResult *Core
var dirResult *Core
var err error

lookUpFile := func() (err error) {
fileResult, err = findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name))
fileResult, err = findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name), false)
return
}
lookUpExplicitDir := func() (err error) {
dirResult, err = findExplicitInode(ctx, d.Bucket(), NewDirName(d.Name(), name))
dirResult, err = findExplicitInode(ctx, d.Bucket(), NewDirName(d.Name(), name), false)
return
}
lookUpImplicitOrExplicitDir := func() (err error) {
dirResult, err = findDirInode(ctx, d.Bucket(), NewDirName(d.Name(), name))
dirResult, err = findDirInode(ctx, d.Bucket(), NewDirName(d.Name(), name), d.isEnableTypeCacheDeprecation)
return
}
lookUpHNSDir := func() (err error) {
dirResult, err = findExplicitFolder(ctx, d.Bucket(), NewDirName(d.Name(), name))
dirResult, err = findExplicitFolder(ctx, d.Bucket(), NewDirName(d.Name(), name), false)
return
}
var cachedType metadata.Type
if d.IsTypeCacheDeprecated() {
// TODO: Add deprecation logic.
cachedType = metadata.UnknownType
} else {
cachedType = d.cache.Get(d.cacheClock.Now(), name)
}

switch cachedType {
case metadata.ImplicitDirType:
dirResult = &Core{
return &Core{
Bucket: d.Bucket(),
FullName: NewDirName(d.Name(), name),
MinObject: nil,
}
}, nil
case metadata.ExplicitDirType:
if d.isBucketHierarchical() {
group.Go(lookUpHNSDir)
Expand All @@ -625,6 +692,7 @@ func (d *dirInode) LookUpChild(ctx context.Context, name string) (*Core, error)
}
case metadata.RegularFileType, metadata.SymlinkType:
group.Go(lookUpFile)

case metadata.NonexistentType:
return nil, nil
case metadata.UnknownType:
Expand All @@ -642,29 +710,16 @@ func (d *dirInode) LookUpChild(ctx context.Context, name string) (*Core, error)
group.Go(lookUpExplicitDir)
}
}

}

if err := group.Wait(); err != nil {
if err = group.Wait(); err != nil {
return nil, err
}

var result *Core
if dirResult != nil {
result = dirResult
} else if fileResult != nil {
result = fileResult
return dirResult, nil
}

if !d.IsTypeCacheDeprecated() {
if result != nil {
d.cache.Insert(d.cacheClock.Now(), name, result.Type())
} else if d.enableNonexistentTypeCache && cachedType == metadata.UnknownType {
d.cache.Insert(d.cacheClock.Now(), name, metadata.NonexistentType)
}
}

return result, nil
return fileResult, nil
}

func (d *dirInode) IsUnlinked() bool {
Expand All @@ -681,10 +736,11 @@ func (d *dirInode) ReadDescendants(ctx context.Context, limit int) (map[Name]*Co
descendants := make(map[Name]*Core)
for {
listing, err := d.bucket.ListObjects(ctx, &gcs.ListObjectsRequest{
Delimiter: "", // recursively
Prefix: d.Name().GcsObjectName(),
ContinuationToken: tok,
MaxResults: limit + 1, // to exclude itself
Delimiter: "", // recursively
Prefix: d.Name().GcsObjectName(),
ContinuationToken: tok,
MaxResults: limit + 1, // to exclude itself
IsTypeCacheDeprecated: d.isEnableTypeCacheDeprecation,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. we should not pass flag values as part of request object for every call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do this then with list call caching will happen correctly with type cache deprecation otherwise implicit directories will not get cached correctly.

})
if err != nil {
return nil, fmt.Errorf("list objects: %w", err)
Expand Down Expand Up @@ -736,6 +792,7 @@ func (d *dirInode) listObjectsAndBuildCores(ctx context.Context, tok string, max
ProjectionVal: gcs.NoAcl,
IncludeFoldersAsPrefixes: d.includeFoldersAsPrefixes,
StartOffset: listStartOffset,
IsTypeCacheDeprecated: d.isEnableTypeCacheDeprecation,
}

listing, err := d.bucket.ListObjects(ctx, req)
Expand Down Expand Up @@ -1209,6 +1266,7 @@ func (d *dirInode) deletePrefixRecursively(ctx context.Context, prefix string) e
Delimiter: "/", // Use Delimiter to separate nested folders (CollapsedRuns)
ContinuationToken: tok,
IncludeFoldersAsPrefixes: d.includeFoldersAsPrefixes,
IsTypeCacheDeprecated: d.isEnableTypeCacheDeprecation,
})
if err != nil {
return fmt.Errorf("listing objects under prefix %q: %w", prefix, err)
Expand Down
Loading
Loading