diff --git a/api/cache/listsession.go b/api/cache/listsession.go
new file mode 100644
index 00000000..bbe4a80b
--- /dev/null
+++ b/api/cache/listsession.go
@@ -0,0 +1,99 @@
+package cache
+
+import (
+ "fmt"
+ "time"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
+ "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
+ cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+ "github.com/bluele/gcache"
+ "go.uber.org/zap"
+)
+
+type (
+ // ListSessionCache contains cache for list session (during pagination).
+ ListSessionCache struct {
+ cache gcache.Cache
+ logger *zap.Logger
+ }
+
+ // ListSessionKey is a key to find a ListSessionCache's entry.
+ ListSessionKey struct {
+ cid cid.ID
+ prefix string
+ token string
+ }
+)
+
+const (
+ // DefaultListSessionCacheLifetime is a default lifetime of entries in cache of ListObjects.
+ DefaultListSessionCacheLifetime = time.Second * 60
+ // DefaultListSessionCacheSize is a default size of cache of ListObjects.
+ DefaultListSessionCacheSize = 100
+)
+
+// DefaultListSessionConfig returns new default cache expiration values.
+func DefaultListSessionConfig(logger *zap.Logger) *Config {
+ return &Config{
+ Size: DefaultListSessionCacheSize,
+ Lifetime: DefaultListSessionCacheLifetime,
+ Logger: logger,
+ }
+}
+
+func (k *ListSessionKey) String() string {
+ return k.cid.EncodeToString() + k.prefix + k.token
+}
+
+// NewListSessionCache is a constructor which creates an object of ListObjectsCache with the given lifetime of entries.
+func NewListSessionCache(config *Config) *ListSessionCache {
+ gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).EvictedFunc(func(key interface{}, val interface{}) {
+ session, ok := val.(*data.ListSession)
+ if !ok {
+ config.Logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", val)),
+ zap.String("expected", fmt.Sprintf("%T", session)))
+ }
+
+ session.Cancel()
+ }).Build()
+ return &ListSessionCache{cache: gc, logger: config.Logger}
+}
+
+// GetListSession returns a list of ObjectInfo.
+func (l *ListSessionCache) GetListSession(key ListSessionKey) *data.ListSession {
+ entry, err := l.cache.Get(key)
+ if err != nil {
+ return nil
+ }
+
+ result, ok := entry.(*data.ListSession)
+ if !ok {
+ l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
+ zap.String("expected", fmt.Sprintf("%T", result)))
+ return nil
+ }
+
+ return result
+}
+
+// PutListSession puts a list of object versions to cache.
+func (l *ListSessionCache) PutListSession(key ListSessionKey, session *data.ListSession) error {
+ return l.cache.Set(key, session)
+}
+
+// DeleteListSession removes key from cache.
+func (l *ListSessionCache) DeleteListSession(key ListSessionKey) {
+ l.cache.Remove(key)
+}
+
+// CreateListSessionCacheKey returns ListSessionKey with the given CID, prefix and token.
+func CreateListSessionCacheKey(cnr cid.ID, prefix, token string) ListSessionKey {
+ p := ListSessionKey{
+ cid: cnr,
+ prefix: prefix,
+ token: token,
+ }
+
+ return p
+}
diff --git a/api/data/listsession.go b/api/data/listsession.go
new file mode 100644
index 00000000..50507390
--- /dev/null
+++ b/api/data/listsession.go
@@ -0,0 +1,18 @@
+package data
+
+import (
+ "context"
+)
+
+type VersionsStream interface {
+ Next(ctx context.Context) (*NodeVersion, error)
+}
+
+// todo consider thread safe
+type ListSession struct {
+ Next *ObjectInfo
+ Stream VersionsStream
+ NamesMap map[string]struct{}
+ Context context.Context
+ Cancel context.CancelFunc
+}
diff --git a/api/layer/cache.go b/api/layer/cache.go
index b4d5fa96..b3c2c565 100644
--- a/api/layer/cache.go
+++ b/api/layer/cache.go
@@ -1,8 +1,6 @@
package layer
import (
- "context"
-
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
@@ -12,22 +10,15 @@ import (
"go.uber.org/zap"
)
-type TestCacheValue struct {
- Next *data.ObjectInfo
- Stream LatestVersionsByPrefixStream
- NamesMap map[string]struct{}
- Context context.Context
-}
-
type Cache struct {
- testCache map[string]TestCacheValue
- logger *zap.Logger
- listsCache *cache.ObjectsListCache
- objCache *cache.ObjectsCache
- namesCache *cache.ObjectsNameCache
- bucketCache *cache.BucketCache
- systemCache *cache.SystemCache
- accessCache *cache.AccessControlCache
+ logger *zap.Logger
+ listsCache *cache.ObjectsListCache
+ sessionListCache *cache.ListSessionCache
+ objCache *cache.ObjectsCache
+ namesCache *cache.ObjectsNameCache
+ bucketCache *cache.BucketCache
+ systemCache *cache.SystemCache
+ accessCache *cache.AccessControlCache
}
// CachesConfig contains params for caches.
@@ -35,6 +26,7 @@ type CachesConfig struct {
Logger *zap.Logger
Objects *cache.Config
ObjectsList *cache.Config
+ SessionList *cache.Config
Names *cache.Config
Buckets *cache.Config
System *cache.Config
@@ -47,6 +39,7 @@ func DefaultCachesConfigs(logger *zap.Logger) *CachesConfig {
Logger: logger,
Objects: cache.DefaultObjectsConfig(logger),
ObjectsList: cache.DefaultObjectsListConfig(logger),
+ SessionList: cache.DefaultListSessionConfig(logger),
Names: cache.DefaultObjectsNameConfig(logger),
Buckets: cache.DefaultBucketConfig(logger),
System: cache.DefaultSystemConfig(logger),
@@ -56,14 +49,14 @@ func DefaultCachesConfigs(logger *zap.Logger) *CachesConfig {
func NewCache(cfg *CachesConfig) *Cache {
return &Cache{
- testCache: map[string]TestCacheValue{},
- logger: cfg.Logger,
- listsCache: cache.NewObjectsListCache(cfg.ObjectsList),
- objCache: cache.New(cfg.Objects),
- namesCache: cache.NewObjectsNameCache(cfg.Names),
- bucketCache: cache.NewBucketCache(cfg.Buckets),
- systemCache: cache.NewSystemCache(cfg.System),
- accessCache: cache.NewAccessControlCache(cfg.AccessControl),
+ logger: cfg.Logger,
+ listsCache: cache.NewObjectsListCache(cfg.ObjectsList),
+ sessionListCache: cache.NewListSessionCache(cfg.ObjectsList),
+ objCache: cache.New(cfg.Objects),
+ namesCache: cache.NewObjectsNameCache(cfg.Names),
+ bucketCache: cache.NewBucketCache(cfg.Buckets),
+ systemCache: cache.NewSystemCache(cfg.System),
+ accessCache: cache.NewAccessControlCache(cfg.AccessControl),
}
}
@@ -155,6 +148,29 @@ func (c *Cache) PutList(owner user.ID, key cache.ObjectsListKey, list []*data.No
}
}
+func (c *Cache) GetListSession(owner user.ID, key cache.ListSessionKey) *data.ListSession {
+ if !c.accessCache.Get(owner, key.String()) {
+ return nil
+ }
+
+ return c.sessionListCache.GetListSession(key)
+}
+
+func (c *Cache) PutListSession(owner user.ID, key cache.ListSessionKey, session *data.ListSession) {
+ if err := c.sessionListCache.PutListSession(key, session); err != nil {
+ c.logger.Warn(logs.CouldntCacheListSession, zap.Error(err))
+ }
+
+ if err := c.accessCache.Put(owner, key.String()); err != nil {
+ c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err))
+ }
+}
+
+func (c *Cache) DeleteListSession(owner user.ID, key cache.ListSessionKey) {
+ c.sessionListCache.DeleteListSession(key)
+ c.accessCache.Delete(owner, key.String())
+}
+
func (c *Cache) GetTagging(owner user.ID, key string) map[string]string {
if !c.accessCache.Get(owner, key) {
return nil
diff --git a/api/layer/object.go b/api/layer/object.go
index 1fb34513..86c48511 100644
--- a/api/layer/object.go
+++ b/api/layer/object.go
@@ -17,7 +17,6 @@ import (
"strconv"
"strings"
"sync"
- "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
@@ -652,41 +651,39 @@ func (n *layer) getLatestObjectsVersionsV2(ctx context.Context, p allObjectParam
return nil, nil, nil
}
- testKey := p.Prefix + p.Delimiter + p.ContinuationToken
- nodeVersionsStreamValue, ok := n.cache.testCache[testKey]
-
- if ok {
- delete(n.cache.testCache, testKey)
+ owner := n.BearerOwner(ctx)
+ cacheKey := cache.CreateListSessionCacheKey(p.Bucket.CID, p.Prefix, p.Delimiter)
+ session := n.cache.GetListSession(owner, cacheKey)
+ if session != nil {
+ // after reading next object from stream in session
+ // the current cache value already doesn't match with next token in cache key
+ n.cache.DeleteListSession(owner, cacheKey)
} else {
- ctx2, cancel2 := context.WithCancel(context.Background())
- go func() {
- <-time.After(10 * time.Second)
- cancel2()
- }()
+ session = &data.ListSession{NamesMap: make(map[string]struct{})}
+ session.Context, session.Cancel = context.WithCancel(context.Background())
if bd, err := middleware.GetBoxData(ctx); err == nil {
- ctx2 = middleware.SetBoxData(ctx2, bd)
+ session.Context = middleware.SetBoxData(session.Context, bd)
}
- nodeVersionsStreamValue.Stream, err = n.treeService.GetLatestVersionsByPrefixStream(ctx2, p.Bucket, p.Prefix)
+ session.Stream, err = n.treeService.GetLatestVersionsByPrefixStream(session.Context, p.Bucket, p.Prefix)
if err != nil {
return nil, nil, err
}
- nodeVersionsStreamValue.NamesMap = map[string]struct{}{}
}
poolCtx, cancel := context.WithCancel(ctx)
defer cancel()
- generator, errorCh := nodesGeneratorStream(poolCtx, p, nodeVersionsStreamValue)
+ generator, errorCh := nodesGeneratorStream(poolCtx, p, session)
objOutCh, err := n.initWorkerPoolStream(poolCtx, 2, p, generator)
if err != nil {
return nil, nil, fmt.Errorf("failed to init worker pool: %w", err)
}
objects = make([]*data.ObjectInfo, 0, p.MaxKeys+1)
- if nodeVersionsStreamValue.Next != nil {
- objects = append(objects, nodeVersionsStreamValue.Next)
+ if session.Next != nil {
+ objects = append(objects, session.Next)
}
for obj := range objOutCh {
@@ -694,9 +691,7 @@ func (n *layer) getLatestObjectsVersionsV2(ctx context.Context, p allObjectParam
}
if err = <-errorCh; err != nil {
- fmt.Println(len(objects))
- fmt.Println(objects[len(objects)-1].Name)
- return nil, nil, fmt.Errorf("failed to get object from tree: %w", err)
+ return nil, nil, fmt.Errorf("failed to get next object from stream: %w", err)
}
sort.Slice(objects, func(i, j int) bool {
@@ -709,8 +704,8 @@ func (n *layer) getLatestObjectsVersionsV2(ctx context.Context, p allObjectParam
}
if next != nil {
- nodeVersionsStreamValue.Next = next
- n.cache.testCache[p.Prefix+p.Delimiter+next.VersionID()] = nodeVersionsStreamValue
+ session.Next = next
+ n.cache.PutListSession(owner, cache.CreateListSessionCacheKey(p.Bucket.CID, p.Prefix, next.VersionID()), session)
}
return
@@ -772,7 +767,7 @@ func nodesGeneratorVersions(ctx context.Context, p allObjectParams, nodeVersions
return nodeCh
}
-func nodesGeneratorStream(ctx context.Context, p allObjectParams, stream TestCacheValue) (<-chan *data.NodeVersion, <-chan error) {
+func nodesGeneratorStream(ctx context.Context, p allObjectParams, stream *data.ListSession) (<-chan *data.NodeVersion, <-chan error) {
nodeCh := make(chan *data.NodeVersion)
errCh := make(chan error, 1)
//existed := make(map[string]struct{}, p.MaxKeys) // to squash the same directories
diff --git a/api/layer/tree_mock.go b/api/layer/tree_mock.go
index 2c556a56..5c55ebe8 100644
--- a/api/layer/tree_mock.go
+++ b/api/layer/tree_mock.go
@@ -212,7 +212,7 @@ func (t *TreeServiceMock) GetLatestVersionsByPrefix(_ context.Context, bktInfo *
return result, nil
}
-func (t *TreeServiceMock) GetLatestVersionsByPrefixStream(ctx context.Context, bktInfo *data.BucketInfo, prefix string) (LatestVersionsByPrefixStream, error) {
+func (t *TreeServiceMock) GetLatestVersionsByPrefixStream(ctx context.Context, bktInfo *data.BucketInfo, prefix string) (data.VersionsStream, error) {
cnrVersionsMap, ok := t.versions[bktInfo.CID.EncodeToString()]
if !ok {
return nil, ErrNodeNotFound
diff --git a/api/layer/tree_service.go b/api/layer/tree_service.go
index f17513e1..c21e6c7a 100644
--- a/api/layer/tree_service.go
+++ b/api/layer/tree_service.go
@@ -8,10 +8,6 @@ import (
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)
-type LatestVersionsByPrefixStream interface {
- Next(ctx context.Context) (*data.NodeVersion, error)
-}
-
// TreeService provide interface to interact with tree service using s3 data models.
type TreeService interface {
// PutSettingsNode update or create new settings node in tree service.
@@ -59,7 +55,7 @@ type TreeService interface {
GetVersions(ctx context.Context, bktInfo *data.BucketInfo, objectName string) ([]*data.NodeVersion, error)
GetLatestVersion(ctx context.Context, bktInfo *data.BucketInfo, objectName string) (*data.NodeVersion, error)
GetLatestVersionsByPrefix(ctx context.Context, bktInfo *data.BucketInfo, prefix string) ([]*data.NodeVersion, error)
- GetLatestVersionsByPrefixStream(ctx context.Context, bktInfo *data.BucketInfo, prefix string) (LatestVersionsByPrefixStream, error)
+ GetLatestVersionsByPrefixStream(ctx context.Context, bktInfo *data.BucketInfo, prefix string) (data.VersionsStream, error)
GetAllVersionsByPrefix(ctx context.Context, bktInfo *data.BucketInfo, prefix string) ([]*data.NodeVersion, error)
GetUnversioned(ctx context.Context, bktInfo *data.BucketInfo, objectName string) (*data.NodeVersion, error)
AddVersion(ctx context.Context, bktInfo *data.BucketInfo, newVersion *data.NodeVersion) (uint64, error)
diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go
index 23b59dff..684cdb1b 100644
--- a/cmd/s3-gw/app.go
+++ b/cmd/s3-gw/app.go
@@ -906,6 +906,9 @@ func getCacheOptions(v *viper.Viper, l *zap.Logger) *layer.CachesConfig {
cacheCfg.ObjectsList.Lifetime = fetchCacheLifetime(v, l, cfgListObjectsCacheLifetime, cacheCfg.ObjectsList.Lifetime)
cacheCfg.ObjectsList.Size = fetchCacheSize(v, l, cfgListObjectsCacheSize, cacheCfg.ObjectsList.Size)
+ cacheCfg.SessionList.Lifetime = fetchCacheLifetime(v, l, cfgSessionListCacheLifetime, cacheCfg.SessionList.Lifetime)
+ cacheCfg.SessionList.Size = fetchCacheSize(v, l, cfgSessionListCacheSize, cacheCfg.SessionList.Size)
+
cacheCfg.Buckets.Lifetime = fetchCacheLifetime(v, l, cfgBucketsCacheLifetime, cacheCfg.Buckets.Lifetime)
cacheCfg.Buckets.Size = fetchCacheSize(v, l, cfgBucketsCacheSize, cacheCfg.Buckets.Size)
diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go
index 3b09ddf1..10cacd91 100644
--- a/cmd/s3-gw/app_settings.go
+++ b/cmd/s3-gw/app_settings.go
@@ -98,6 +98,8 @@ const ( // Settings.
cfgObjectsCacheSize = "cache.objects.size"
cfgListObjectsCacheLifetime = "cache.list.lifetime"
cfgListObjectsCacheSize = "cache.list.size"
+ cfgSessionListCacheLifetime = "cache.list_session.lifetime"
+ cfgSessionListCacheSize = "cache.list_session.size"
cfgBucketsCacheLifetime = "cache.buckets.lifetime"
cfgBucketsCacheSize = "cache.buckets.size"
cfgNamesCacheLifetime = "cache.names.lifetime"
diff --git a/config/config.env b/config/config.env
index 037a4262..6ce53864 100644
--- a/config/config.env
+++ b/config/config.env
@@ -82,6 +82,9 @@ S3_GW_CACHE_OBJECTS_SIZE=1000000
# Cache which keeps lists of objects in buckets
S3_GW_CACHE_LIST_LIFETIME=1m
S3_GW_CACHE_LIST_SIZE=100000
+# Cache which keeps listing session
+S3_GW_CACHE_LIST_SESSION_LIFETIME=1m
+S3_GW_CACHE_LIST_SESSION_SIZE=100
# Cache which contains mapping of bucket name to bucket info
S3_GW_CACHE_BUCKETS_LIFETIME=1m
S3_GW_CACHE_BUCKETS_SIZE=1000
diff --git a/config/config.yaml b/config/config.yaml
index 06eb9628..c1b1ba8f 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -100,6 +100,10 @@ cache:
list:
lifetime: 1m
size: 100
+ # Cache which keeps listing sessions
+ list_session:
+ lifetime: 1m
+ size: 100
# Cache which contains mapping of nice name to object addresses
names:
lifetime: 1m
diff --git a/docs/configuration.md b/docs/configuration.md
index e7d5adf1..2ec1da5a 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -396,6 +396,9 @@ cache:
list:
lifetime: 1m
size: 100
+ list_session:
+ lifetime: 1m
+ size: 100
names:
lifetime: 1m
size: 1000
@@ -420,6 +423,7 @@ cache:
|-----------------|-----------------------------------|-----------------------------------|----------------------------------------------------------------------------------------|
| `objects` | [Cache config](#cache-subsection) | `lifetime: 5m`
`size: 1000000` | Cache for objects (FrostFS headers). |
| `list` | [Cache config](#cache-subsection) | `lifetime: 60s`
`size: 100000` | Cache which keeps lists of objects in buckets. |
+| `list_session` | [Cache config](#cache-subsection) | `lifetime: 60s`
`size: 100` | Cache which keeps listing session. |
| `names` | [Cache config](#cache-subsection) | `lifetime: 60s`
`size: 10000` | Cache which contains mapping of nice name to object addresses. |
| `buckets` | [Cache config](#cache-subsection) | `lifetime: 60s`
`size: 1000` | Cache which contains mapping of bucket name to bucket info. |
| `system` | [Cache config](#cache-subsection) | `lifetime: 5m`
`size: 10000` | Cache for system objects in a bucket: bucket settings, notification configuration etc. |
diff --git a/internal/logs/logs.go b/internal/logs/logs.go
index c3ca1fa7..2e4954ce 100644
--- a/internal/logs/logs.go
+++ b/internal/logs/logs.go
@@ -95,6 +95,7 @@ const (
CouldntCacheAccessControlOperation = "couldn't cache access control operation" // Warn in ../../api/layer/cache.go
CouldntPutObjAddressToNameCache = "couldn't put obj address to name cache" // Warn in ../../api/layer/cache.go
CouldntCacheListOfObjects = "couldn't cache list of objects" // Warn in ../../api/layer/cache.go
+ CouldntCacheListSession = "couldn't cache list session" // Warn in ../../api/layer/cache.go
CouldntCacheTags = "couldn't cache tags" // Error in ../../api/layer/cache.go
CouldntCacheLockInfo = "couldn't cache lock info" // Error in ../../api/layer/cache.go
CouldntCacheBucketSettings = "couldn't cache bucket settings" // Warn in ../../api/layer/cache.go
diff --git a/pkg/service/tree/tree.go b/pkg/service/tree/tree.go
index 7c3ce2f8..dba9bf6c 100644
--- a/pkg/service/tree/tree.go
+++ b/pkg/service/tree/tree.go
@@ -740,7 +740,7 @@ func (s *LatestVersionsByPrefixStreamImpl) Next(ctx context.Context) (*data.Node
return newNodeVersionFromTreeNode(filepath, treeNode), nil
}
-func (c *Tree) GetLatestVersionsByPrefixStream(ctx context.Context, bktInfo *data.BucketInfo, prefix string) (layer.LatestVersionsByPrefixStream, error) {
+func (c *Tree) GetLatestVersionsByPrefixStream(ctx context.Context, bktInfo *data.BucketInfo, prefix string) (data.VersionsStream, error) {
mainStream, tailPrefix, rootID, err := c.getSubTreeByPrefixMainStream(ctx, bktInfo, versionTree, prefix)
if err != nil {
if errors.Is(err, io.EOF) {