Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Folder metadata in stat cache #2097

Merged
merged 19 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
101 changes: 91 additions & 10 deletions internal/cache/metadata/stat_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"math"
"time"

"cloud.google.com/go/storage/control/apiv2/controlpb"
"github.com/googlecloudplatform/gcsfuse/v2/internal/cache/lru"
"github.com/googlecloudplatform/gcsfuse/v2/internal/storage/gcs"
"github.com/googlecloudplatform/gcsfuse/v2/internal/util"
Expand Down Expand Up @@ -45,10 +46,29 @@ type StatCache interface {
// Erase the entry for the given object name, if any.
Erase(name string)

// Return the current entry for the given name, or nil if there is a negative
// Return the current object entry for the given name, or nil if there is a negative
// entry. Return hit == false when there is neither a positive nor a negative
// entry, or the entry has expired according to the supplied current time.
LookUp(name string, now time.Time) (hit bool, m *gcs.MinObject)

// Insert an entry for the given folder resource.
//
// In order to help cope with caching of arbitrarily out of date (i.e.
// inconsistent) object listings, entry will not replace any positive entry
// with a newer meta generation number.
//
// The entry will expire after the supplied time.
InsertFolder(f *controlpb.Folder, expiration time.Time)

// Return the current folder entry for the given name, or nil if there is a negative
// entry. Return hit == false when there is neither a positive nor a negative
// entry, or the entry has expired according to the supplied current time.
LookUpFolder(folderName string, now time.Time) (bool, *controlpb.Folder)

// Set up a negative entry for the given folder name, indicating that the name
// doesn't exist. Overwrite any existing entry for the name, positive or
// negative.
AddNegativeEntryForFolder(folderName string, expiration time.Time)
}

// Create a new bucket-view to the passed shared-cache object.
Expand Down Expand Up @@ -80,6 +100,7 @@ type statCacheBucketView struct {
// entry. Nil object means negative entry.
type entry struct {
m *gcs.MinObject
f *controlpb.Folder
expiration time.Time
key string
}
Expand All @@ -92,14 +113,18 @@ type entry struct {
// benchmark runs) to heap-size per positive stat-cache entry
// to calculate a size closer to the actual memory utilization.
func (e entry) Size() (size uint64) {
// First, calculate size on heap.
// First, calculate size on heap (including folder size also in case of hns buckets, in case of non-hns buckets 0 will be added as e.f will be Nil ).
// Additional 2*util.UnsafeSizeOf(&e.key) is to account for the copies of string
// struct stored in the cache map and in the cache linked-list.
size = uint64(util.UnsafeSizeOf(&e) + len(e.key) + 2*util.UnsafeSizeOf(&e.key) + util.NestedSizeOfGcsMinObject(e.m))
if e.m != nil {
size += 515
}

if e.f != nil {
size = size + uint64(util.UnsafeSizeOf(&e.f))
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved
}

// Convert heap-size to RSS (resident set size).
size = uint64(math.Ceil(util.HeapSizeToRssConversionFactor * float64(size)))

Expand Down Expand Up @@ -176,30 +201,86 @@ func (sc *statCacheBucketView) AddNegativeEntry(objectName string, expiration ti
}
}

func (sc *statCacheBucketView) AddNegativeEntryForFolder(folderName string, expiration time.Time) {
name := sc.key(folderName)

// Insert a negative entry.
e := entry{
f: nil,
expiration: expiration,
key: name,
}

if _, err := sc.sharedCache.Insert(name, e); err != nil {
panic(err)
}
}

func (sc *statCacheBucketView) Erase(objectName string) {
name := sc.key(objectName)
sc.sharedCache.Erase(name)
}

func (sc *statCacheBucketView) LookUp(
objectName string,
now time.Time) (hit bool, m *gcs.MinObject) {
now time.Time) (bool, *gcs.MinObject) {
// Look up in the LRU cache.
value := sc.sharedCache.LookUp(sc.key(objectName))
entry := sc.lruCacheLookup(objectName, now)
if entry == nil {
return false, nil
}

return true, entry.m
}

Tulsishah marked this conversation as resolved.
Show resolved Hide resolved
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved
func (sc *statCacheBucketView) LookUpFolder(
folderName string,
now time.Time) (bool, *controlpb.Folder) {
// Look up in the LRU cache.
entry := sc.lruCacheLookup(folderName, now)
if entry == nil {
return false, nil
}

return true, entry.f
}

func (sc *statCacheBucketView) lruCacheLookup(key string, now time.Time) *entry {
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved
value := sc.sharedCache.LookUp(sc.key(key))
if value == nil {
return
return nil
}

e := value.(entry)

// Has this entry expired?
if e.expiration.Before(now) {
sc.Erase(objectName)
return
sc.Erase(key)
return nil
}

hit = true
m = e.m
return &e
}

return
func (sc *statCacheBucketView) InsertFolder(f *controlpb.Folder, expiration time.Time) {
name := sc.key(f.Name)

// Return if there is already a better entry?
existing := sc.sharedCache.LookUp(name)
if existing != nil && existing.(entry).f != nil {
existingFolder := existing.(entry).f
if f.Metageneration != existingFolder.Metageneration && f.Metageneration < existingFolder.Metageneration {
return
}
}

e := entry{
f: f,
expiration: expiration,
key: name,
}

if _, err := sc.sharedCache.Insert(name, e); err != nil {
panic(err)
}
}
130 changes: 129 additions & 1 deletion internal/cache/metadata/stat_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"
"time"

"cloud.google.com/go/storage/control/apiv2/controlpb"
"github.com/googlecloudplatform/gcsfuse/v2/internal/cache/lru"
"github.com/googlecloudplatform/gcsfuse/v2/internal/cache/metadata"
"github.com/googlecloudplatform/gcsfuse/v2/internal/mount"
Expand Down Expand Up @@ -100,7 +101,8 @@ var expiration = someTime.Add(time.Second)

type StatCacheTest struct {
suite.Suite
cache testHelperCache
cache testHelperCache
statCache metadata.StatCache
}

type MultiBucketStatCacheTest struct {
Expand All @@ -111,6 +113,7 @@ type MultiBucketStatCacheTest struct {
func (t *StatCacheTest) SetupTest() {
cache := lru.NewCache(uint64((mount.AverageSizeOfPositiveStatCacheEntry + mount.AverageSizeOfNegativeStatCacheEntry) * capacity))
t.cache.wrapped = metadata.NewStatCacheBucketView(cache, "") // this demonstrates
t.statCache = metadata.NewStatCacheBucketView(cache, "") // this demonstrates
Tulsishah marked this conversation as resolved.
Show resolved Hide resolved
// that if you are using a cache for a single bucket, then
// its prepending bucketName can be left empty("") without any problem.
}
Expand Down Expand Up @@ -408,3 +411,128 @@ func (t *MultiBucketStatCacheTest) Test_ExpiresLeastRecentlyUsed() {
assert.Equal(t.T(), cardamom, spices.LookUpOrNil("cardamom", someTime))
assert.Equal(t.T(), saffron, spices.LookUpOrNil("saffron", someTime))
}

func (t *StatCacheTest) Test_Create_Entry_When_No_Entry_Is_Present() {
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved
const name = "key1"
newEntry := &controlpb.Folder{
Name: name,
Metageneration: 1,
}

t.statCache.InsertFolder(newEntry, expiration)

hit, entry := t.statCache.LookUpFolder(name, someTime)
assert.True(t.T(), hit)
assert.Equal(t.T(), "key1", entry.Name)
assert.Equal(t.T(), int64(1), entry.Metageneration)
}

func (t *StatCacheTest) Test_Override_Entry_Old_Entry_Is_Already_Present() {
const name = "key1"
existingEntry := &controlpb.Folder{
Name: name,
Metageneration: 1,
}
t.statCache.InsertFolder(existingEntry, expiration)
vadlakondaswetha marked this conversation as resolved.
Show resolved Hide resolved
newEntry := &controlpb.Folder{
Name: name,
Metageneration: 2,
}

t.statCache.InsertFolder(newEntry, expiration)

hit, entry := t.statCache.LookUpFolder(name, someTime)
assert.True(t.T(), hit)
assert.Equal(t.T(), "key1", entry.Name)
assert.Equal(t.T(), int64(2), entry.Metageneration)
}

func (t *StatCacheTest) Test_Lookup_Return_False_If_Expiration_Is_Passed() {
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved
const name = "key1"
entry := &controlpb.Folder{
Name: name,
Metageneration: 1,
}
t.statCache.InsertFolder(entry, expiration)

hit, result := t.statCache.LookUpFolder(name, expiration.Add(time.Second))

assert.False(t.T(), hit)
assert.Nil(t.T(), result)
}

func (t *StatCacheTest) Test_Lookup_Return_False_When_Is_Not_Present() {
const name = "key1"

hit, result := t.statCache.LookUpFolder(name, expiration.Add(time.Second))

assert.False(t.T(), hit)
assert.Nil(t.T(), result)
}

func (t *StatCacheTest) Test_Should_Not_Override_Entry_If_Metageneration_Is_Old() {
const name = "key1"
existingEntry := &controlpb.Folder{
Name: name,
Metageneration: 2,
}
t.statCache.InsertFolder(existingEntry, expiration)
newEntry := &controlpb.Folder{
Name: name,
Metageneration: 1,
}

t.statCache.InsertFolder(newEntry, expiration)

hit, entry := t.statCache.LookUpFolder(name, someTime)
assert.True(t.T(), hit)
assert.Equal(t.T(), "key1", entry.Name)
assert.Equal(t.T(), int64(2), entry.Metageneration)
}

func (t *StatCacheTest) Test_Should_Add_Negative_Entry_For_Folder() {
const name = "key1"
existingEntry := &controlpb.Folder{
Name: name,
Metageneration: 2,
}
t.statCache.InsertFolder(existingEntry, expiration)

t.statCache.AddNegativeEntryForFolder(name, expiration)

hit, entry := t.statCache.LookUpFolder(name, someTime)
assert.True(t.T(), hit)
assert.Nil(t.T(), entry)
}

func (t *StatCacheTest) Test_Should_Evict_Entry_On_Full_Capacity_Including_Folder_Size() {
localCache := lru.NewCache(uint64(3000))
t.statCache = metadata.NewStatCacheBucketView(localCache, "local_bucket")
objectEntry1 := &gcs.MinObject{Name: "1"}
objectEntry2 := &gcs.MinObject{Name: "2"}
folderEntry := &controlpb.Folder{
Name: "3",
Metageneration: 1,
}
t.statCache.Insert(objectEntry1, expiration) // adds size of 1428
t.statCache.Insert(objectEntry2, expiration) // adds size of 1428

hit1, entry1 := t.statCache.LookUp("1", someTime)
hit2, entry2 := t.statCache.LookUp("2", someTime)

assert.True(t.T(), hit1)
assert.Equal(t.T(), "1", entry1.Name)
assert.True(t.T(), hit2)
assert.Equal(t.T(), "2", entry2.Name)

t.statCache.InsertFolder(folderEntry, expiration) //adds size of 220 and exceeds capacity

hit1, entry1 = t.statCache.LookUp("1", someTime)
hit3, entry3 := t.statCache.LookUpFolder("3", someTime)
Tulsishah marked this conversation as resolved.
Show resolved Hide resolved
ankitaluthra1 marked this conversation as resolved.
Show resolved Hide resolved

assert.False(t.T(), hit1)
assert.Nil(t.T(), entry1)
assert.True(t.T(), hit3)
assert.Equal(t.T(), "3", entry3.Name)

}
64 changes: 64 additions & 0 deletions internal/storage/caching/mock_gcscaching/mock_stat_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
time "time"
unsafe "unsafe"

"cloud.google.com/go/storage/control/apiv2/controlpb"
"github.com/googlecloudplatform/gcsfuse/v2/internal/cache/metadata"
"github.com/googlecloudplatform/gcsfuse/v2/internal/storage/gcs"
oglemock "github.com/jacobsa/oglemock"
Expand All @@ -27,6 +28,23 @@ type mockStatCache struct {
description string
}

func (m *mockStatCache) AddNegativeEntryForFolder(p0 string, p1 time.Time) {
// Get a folder name and line number for the caller.
_, name, line, _ := runtime.Caller(1)

// Hand the call off to the controller, which does most of the work.
retVals := m.controller.HandleMethodCall(
m,
"AddNegativeEntryForFolder",
name,
line,
[]interface{}{p0, p1})

if len(retVals) != 0 {
panic(fmt.Sprintf("mockStatCache.AddNegativeEntryforFolder: invalid return values: %v", retVals))
}
}

func NewMockStatCache(
c oglemock.Controller,
desc string) MockStatCache {
Expand Down Expand Up @@ -123,3 +141,49 @@ func (m *mockStatCache) LookUp(p0 string, p1 time.Time) (o0 bool, o1 *gcs.MinObj

return
}

func (m *mockStatCache) InsertFolder(p0 *controlpb.Folder, p1 time.Time) {
// Get a file name and line number for the caller.
_, file, line, _ := runtime.Caller(1)

// Hand the call off to the controller, which does most of the work.
retVals := m.controller.HandleMethodCall(
m,
"InsertFolder",
file,
line,
[]interface{}{p0, p1})

if len(retVals) != 0 {
panic(fmt.Sprintf("mockStatCache.InsertFolder: invalid return values: %v", retVals))
}
}

func (m *mockStatCache) LookUpFolder(p0 string, p1 time.Time) (o0 bool, o1 *controlpb.Folder) {
// Get a file name and line number for the caller.
_, file, line, _ := runtime.Caller(1)

// Hand the call off to the controller, which does most of the work.
retVals := m.controller.HandleMethodCall(
m,
"LookUpFolder",
file,
line,
[]interface{}{p0, p1})

if len(retVals) != 2 {
panic(fmt.Sprintf("mockStatCache.LookUpFolder: invalid return values: %v", retVals))
}

// o0 bool
if retVals[0] != nil {
o0 = retVals[0].(bool)
}

// o1 *controlpb.Folder
if retVals[1] != nil {
o1 = retVals[1].(*controlpb.Folder)
}

return
}
Loading