20318: Add config entry for keep-web cache size.
authorTom Clegg <tom@curii.com>
Fri, 22 Dec 2023 16:11:51 +0000 (11:11 -0500)
committerTom Clegg <tom@curii.com>
Tue, 26 Dec 2023 20:44:44 +0000 (15:44 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

14 files changed:
lib/config/config.default.yml
lib/config/deprecated.go
lib/config/deprecated_test.go
sdk/go/arvados/byte_size.go
sdk/go/arvados/byte_size_test.go
sdk/go/arvados/client.go
sdk/go/arvados/config.go
sdk/go/arvados/keep_cache.go
sdk/go/arvados/keep_cache_test.go
sdk/go/arvadosclient/arvadosclient.go
sdk/go/keepclient/keepclient.go
services/keep-web/cache.go
services/keep-web/handler.go
services/keep-web/handler_test.go

index 05bc1309cdde1ef269306fb634dbe4e6595914ee..872501f9153ea2201bfbea57aa65258a7c0b8b84 100644 (file)
@@ -721,16 +721,18 @@ Clusters:
         # Time to cache manifests, permission checks, and sessions.
         TTL: 300s
 
-        # Block cache entries. Each block consumes up to 64 MiB RAM.
-        MaxBlockEntries: 20
+        # Maximum amount of data cached in /var/cache/arvados/keep.
+        # Can be given as a percentage ("10%") or a number of bytes
+        # ("10 GiB")
+        DiskCacheSize: 10%
 
         # Approximate memory limit (in bytes) for session cache.
         #
         # Note this applies to the in-memory representation of
         # projects and collections -- metadata, block locators,
-        # filenames, etc. -- excluding cached file content, which is
-        # limited by MaxBlockEntries.
-        MaxCollectionBytes: 100000000
+        # filenames, etc. -- not the file data itself (see
+        # DiskCacheSize).
+        MaxCollectionBytes: 100 MB
 
         # Persistent sessions.
         MaxSessions: 100
index d5c09d67061115a44cb5c8b5ef2a195fa7652734..d518b3414ad193795179f7e9776bccc26fedc129 100644 (file)
@@ -495,7 +495,7 @@ func (ldr *Loader) loadOldKeepWebConfig(cfg *arvados.Config) error {
                cluster.Collections.WebDAVCache.TTL = *oc.Cache.TTL
        }
        if oc.Cache.MaxCollectionBytes != nil {
-               cluster.Collections.WebDAVCache.MaxCollectionBytes = *oc.Cache.MaxCollectionBytes
+               cluster.Collections.WebDAVCache.MaxCollectionBytes = arvados.ByteSize(*oc.Cache.MaxCollectionBytes)
        }
        if oc.AnonymousTokens != nil {
                if len(*oc.AnonymousTokens) > 0 {
index f9b1d1661b1f3c16b745c7e7c6e9304f060e2c64..e06a1f231d96887467759087c14f8fb74b4b3e87 100644 (file)
@@ -199,7 +199,7 @@ func (s *LoadSuite) TestLegacyKeepWebConfig(c *check.C) {
        c.Check(cluster.SystemRootToken, check.Equals, "abcdefg")
 
        c.Check(cluster.Collections.WebDAVCache.TTL, check.Equals, arvados.Duration(60*time.Second))
-       c.Check(cluster.Collections.WebDAVCache.MaxCollectionBytes, check.Equals, int64(1234567890))
+       c.Check(cluster.Collections.WebDAVCache.MaxCollectionBytes, check.Equals, arvados.ByteSize(1234567890))
 
        c.Check(cluster.Services.WebDAVDownload.ExternalURL, check.Equals, arvados.URL{Host: "download.example.com", Path: "/"})
        c.Check(cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: ":80"}], check.NotNil)
index 08cc83e126952e6349bb294c0493899253124107..7cc2c697811a682dd60de00449b3e0b7a7e8b066 100644 (file)
@@ -8,11 +8,16 @@ import (
        "encoding/json"
        "fmt"
        "math"
+       "strconv"
        "strings"
 )
 
 type ByteSize int64
 
+// ByteSizeOrPercent indicates either a number of bytes or a
+// percentage from 1 to 100.
+type ByteSizeOrPercent ByteSize
+
 var prefixValue = map[string]int64{
        "":   1,
        "K":  1000,
@@ -89,3 +94,54 @@ func (n *ByteSize) UnmarshalJSON(data []byte) error {
                return fmt.Errorf("bug: json.Number for %q is not int64 or float64: %s", s, err)
        }
 }
+
+func (n ByteSizeOrPercent) MarshalJSON() ([]byte, error) {
+       if n < 0 && n >= -100 {
+               return []byte(fmt.Sprintf("\"%d%%\"", -n)), nil
+       } else {
+               return json.Marshal(int64(n))
+       }
+}
+
+func (n *ByteSizeOrPercent) UnmarshalJSON(data []byte) error {
+       if len(data) == 0 || data[0] != '"' {
+               return (*ByteSize)(n).UnmarshalJSON(data)
+       }
+       var s string
+       err := json.Unmarshal(data, &s)
+       if err != nil {
+               return err
+       }
+       if s := strings.TrimSpace(s); len(s) > 0 && s[len(s)-1] == '%' {
+               pct, err := strconv.ParseInt(strings.TrimSpace(s[:len(s)-1]), 10, 64)
+               if err != nil {
+                       return err
+               }
+               if pct < 0 || pct > 100 {
+                       return fmt.Errorf("invalid value %q (percentage must be between 0 and 100)", s)
+               }
+               *n = ByteSizeOrPercent(-pct)
+               return nil
+       }
+       return (*ByteSize)(n).UnmarshalJSON(data)
+}
+
+// ByteSize returns the absolute byte size specified by n, or 0 if n
+// specifies a percent.
+func (n ByteSizeOrPercent) ByteSize() ByteSize {
+       if n >= -100 && n < 0 {
+               return 0
+       } else {
+               return ByteSize(n)
+       }
+}
+
+// ByteSize returns the percentage specified by n, or 0 if n specifies
+// an absolute byte size.
+func (n ByteSizeOrPercent) Percent() int64 {
+       if n >= -100 && n < 0 {
+               return int64(-n)
+       } else {
+               return 0
+       }
+}
index 7c4aff207258aab5e4c8e9dfc266089004559831..e5fb10ebdb352137f3abd974b488503565c82671 100644 (file)
@@ -64,7 +64,54 @@ func (s *ByteSizeSuite) TestUnmarshal(c *check.C) {
        } {
                var n ByteSize
                err := yaml.Unmarshal([]byte(testcase+"\n"), &n)
-               c.Logf("%v => error: %v", n, err)
+               c.Logf("%s => error: %v", testcase, err)
+               c.Check(err, check.NotNil)
+       }
+}
+
+func (s *ByteSizeSuite) TestMarshalByteSizeOrPercent(c *check.C) {
+       for _, testcase := range []struct {
+               in  ByteSizeOrPercent
+               out string
+       }{
+               {0, "0"},
+               {-1, "1%"},
+               {-100, "100%"},
+               {8, "8"},
+       } {
+               out, err := yaml.Marshal(&testcase.in)
+               c.Check(err, check.IsNil)
+               c.Check(string(out), check.Equals, testcase.out+"\n")
+       }
+}
+
+func (s *ByteSizeSuite) TestUnmarshalByteSizeOrPercent(c *check.C) {
+       for _, testcase := range []struct {
+               in  string
+               out int64
+       }{
+               {"0", 0},
+               {"100", 100},
+               {"0%", 0},
+               {"1%", -1},
+               {"100%", -100},
+               {"8 GB", 8000000000},
+       } {
+               var n ByteSizeOrPercent
+               err := yaml.Unmarshal([]byte(testcase.in+"\n"), &n)
+               c.Logf("%v => %v: %v", testcase.in, testcase.out, n)
+               c.Check(err, check.IsNil)
+               c.Check(int64(n), check.Equals, testcase.out)
+       }
+       for _, testcase := range []string{
+               "1000%", "101%", "-1%",
+               "%", "-%", "%%", "%1",
+               "400000 EB",
+               "4.11e4 EB",
+       } {
+               var n ByteSizeOrPercent
+               err := yaml.Unmarshal([]byte(testcase+"\n"), &n)
+               c.Logf("%s => error: %v", testcase, err)
                c.Check(err, check.NotNil)
        }
 }
index e3c14326600189ed92f0b5af14db83f244d010f5..991de1caa90c4e2e98b96015415ebecb44525af8 100644 (file)
@@ -77,6 +77,11 @@ type Client struct {
        // context deadline to establish a maximum request time.
        Timeout time.Duration
 
+       // Maximum disk cache size in bytes or percent of total
+       // filesystem size. If zero, use default, currently 10% of
+       // filesystem size.
+       DiskCacheSize ByteSizeOrPercent
+
        dd *DiscoveryDocument
 
        defaultRequestID string
@@ -154,6 +159,7 @@ func NewClientFromConfig(cluster *Cluster) (*Client, error) {
                APIHost:        ctrlURL.Host,
                Insecure:       cluster.TLS.Insecure,
                Timeout:        5 * time.Minute,
+               DiskCacheSize:  cluster.Collections.WebDAVCache.DiskCacheSize,
                requestLimiter: &requestLimiter{maxlimit: int64(cluster.API.MaxConcurrentRequests / 4)},
        }, nil
 }
index 6301ed047a1dbfca82b3c717926a2f05415aa291..acc091a90fdf8d458ac1ae6d181ef3c52d582139 100644 (file)
@@ -63,8 +63,8 @@ func (sc *Config) GetCluster(clusterID string) (*Cluster, error) {
 
 type WebDAVCacheConfig struct {
        TTL                Duration
-       MaxBlockEntries    int
-       MaxCollectionBytes int64
+       DiskCacheSize      ByteSizeOrPercent
+       MaxCollectionBytes ByteSize
        MaxSessions        int
 }
 
index af80daa2e07647346fd628eda6b7acdbc2338295..a6571538761ef2411047c5ce97fd42122a93e6e6 100644 (file)
@@ -37,7 +37,7 @@ type KeepGateway interface {
 type DiskCache struct {
        KeepGateway
        Dir     string
-       MaxSize int64
+       MaxSize ByteSizeOrPercent
        Logger  logrus.FieldLogger
 
        tidying        int32 // see tidy()
@@ -534,7 +534,7 @@ func (cache *DiskCache) gotidy() {
        // is below MaxSize, and we haven't reached the "recheck
        // anyway" time threshold.
        if cache.sizeMeasured > 0 &&
-               atomic.LoadInt64(&cache.sizeEstimated) < cache.MaxSize &&
+               atomic.LoadInt64(&cache.sizeEstimated) < atomic.LoadInt64(&cache.defaultMaxSize) &&
                time.Now().Before(cache.tidyHoldUntil) {
                atomic.AddInt32(&cache.tidying, -1)
                return
@@ -548,14 +548,26 @@ func (cache *DiskCache) gotidy() {
 
 // Delete cache files as needed to control disk usage.
 func (cache *DiskCache) tidy() {
-       maxsize := cache.MaxSize
+       maxsize := int64(cache.MaxSize.ByteSize())
        if maxsize < 1 {
-               if maxsize = atomic.LoadInt64(&cache.defaultMaxSize); maxsize == 0 {
+               maxsize = atomic.LoadInt64(&cache.defaultMaxSize)
+               if maxsize == 0 {
+                       // defaultMaxSize not yet computed. Use 10% of
+                       // filesystem capacity (or different
+                       // percentage if indicated by cache.MaxSize)
+                       pct := cache.MaxSize.Percent()
+                       if pct == 0 {
+                               pct = 10
+                       }
                        var stat unix.Statfs_t
                        if nil == unix.Statfs(cache.Dir, &stat) {
-                               maxsize = int64(stat.Bavail) * stat.Bsize / 10
+                               maxsize = int64(stat.Bavail) * stat.Bsize * pct / 100
+                               atomic.StoreInt64(&cache.defaultMaxSize, maxsize)
+                       } else {
+                               // In this case we will set
+                               // defaultMaxSize below after
+                               // measuring current usage.
                        }
-                       atomic.StoreInt64(&cache.defaultMaxSize, maxsize)
                }
        }
 
@@ -611,7 +623,8 @@ func (cache *DiskCache) tidy() {
 
        // If MaxSize wasn't specified and we failed to come up with a
        // defaultSize above, use the larger of {current cache size, 1
-       // GiB} as the defaultSize for subsequent tidy() operations.
+       // GiB} as the defaultMaxSize for subsequent tidy()
+       // operations.
        if maxsize == 0 {
                if totalsize < 1<<30 {
                        atomic.StoreInt64(&cache.defaultMaxSize, 1<<30)
index e4d1790cac120e286e8b275de181cf066a736c37..ffca531cd722bb784152698ff553f1f906548d40 100644 (file)
@@ -201,7 +201,7 @@ func (s *keepCacheSuite) testConcurrentReaders(c *check.C, cannotRefresh, mangle
        backend := &keepGatewayMemoryBacked{}
        cache := DiskCache{
                KeepGateway: backend,
-               MaxSize:     int64(blksize),
+               MaxSize:     ByteSizeOrPercent(blksize),
                Dir:         c.MkDir(),
                Logger:      ctxlog.TestLogger(c),
        }
@@ -286,7 +286,7 @@ func (s *keepCacheSuite) TestStreaming(c *check.C) {
        }
        cache := DiskCache{
                KeepGateway: backend,
-               MaxSize:     int64(blksize),
+               MaxSize:     ByteSizeOrPercent(blksize),
                Dir:         c.MkDir(),
                Logger:      ctxlog.TestLogger(c),
        }
@@ -354,7 +354,7 @@ func (s *keepCacheBenchSuite) SetUpTest(c *check.C) {
        s.backend = &keepGatewayMemoryBacked{}
        s.cache = &DiskCache{
                KeepGateway: s.backend,
-               MaxSize:     int64(s.blksize),
+               MaxSize:     ByteSizeOrPercent(s.blksize),
                Dir:         c.MkDir(),
                Logger:      ctxlog.TestLogger(c),
        }
index 461320eca90a210d61461b05202f40d2d6d8e774..d0ebdc1b018b9c4039ed0319b77f671b9e5364fe 100644 (file)
@@ -105,6 +105,11 @@ type ArvadosClient struct {
        // available services.
        KeepServiceURIs []string
 
+       // Maximum disk cache size in bytes or percent of total
+       // filesystem size. If zero, use default, currently 10% of
+       // filesystem size.
+       DiskCacheSize arvados.ByteSizeOrPercent
+
        // Discovery document
        DiscoveryDoc Dict
 
@@ -144,6 +149,7 @@ func New(c *arvados.Client) (*ArvadosClient, error) {
                Client:            hc,
                Retries:           2,
                KeepServiceURIs:   c.KeepServiceURIs,
+               DiskCacheSize:     c.DiskCacheSize,
                lastClosedIdlesAt: time.Now(),
        }
 
index 08fa455ff42233d5d4418e4495ba93ac77178c37..b03362ee48ad3b81768a78d672a157457890c216 100644 (file)
@@ -113,6 +113,7 @@ type KeepClient struct {
        RequestID             string
        StorageClasses        []string
        DefaultStorageClasses []string // Set by cluster's exported config
+       DiskCacheSize         arvados.ByteSizeOrPercent
 
        // set to 1 if all writable services are of disk type, otherwise 0
        replicasPerService int
@@ -137,7 +138,6 @@ func (kc *KeepClient) Clone() *KeepClient {
                gatewayRoots:          kc.gatewayRoots,
                HTTPClient:            kc.HTTPClient,
                Retries:               kc.Retries,
-               BlockCache:            kc.BlockCache,
                RequestID:             kc.RequestID,
                StorageClasses:        kc.StorageClasses,
                DefaultStorageClasses: kc.DefaultStorageClasses,
@@ -387,6 +387,7 @@ func (kc *KeepClient) upstreamGateway() arvados.KeepGateway {
        }
        kc.gatewayStack = &arvados.DiskCache{
                Dir:         cachedir,
+               MaxSize:     kc.DiskCacheSize,
                KeepGateway: &keepViaHTTP{kc},
        }
        return kc.gatewayStack
index df5705ed326aa32f4dce2ee1969c1190f8507c6f..d443bc0829f81098715254e4b0e08cd7f5ac87bb 100644 (file)
@@ -303,7 +303,7 @@ func (c *cache) pruneSessions() {
        // Mark more sessions for deletion until reaching desired
        // memory size limit, starting with the oldest entries.
        for i, snap := range snaps {
-               if size <= c.cluster.Collections.WebDAVCache.MaxCollectionBytes {
+               if size <= int64(c.cluster.Collections.WebDAVCache.MaxCollectionBytes) {
                        break
                }
                if snap.prune {
index 123c4fe34da3d1dba8ae504b9867a410cd5022b5..12c2839f8ca78fab56e8111efc2d3111e4bd02b0 100644 (file)
@@ -27,15 +27,13 @@ import (
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
-       "git.arvados.org/arvados.git/sdk/go/keepclient"
        "github.com/sirupsen/logrus"
        "golang.org/x/net/webdav"
 )
 
 type handler struct {
-       Cache     cache
-       Cluster   *arvados.Cluster
-       setupOnce sync.Once
+       Cache   cache
+       Cluster *arvados.Cluster
 
        lockMtx    sync.Mutex
        lock       map[string]*sync.RWMutex
@@ -60,10 +58,6 @@ func parseCollectionIDFromURL(s string) string {
        return ""
 }
 
-func (h *handler) setup() {
-       keepclient.DefaultBlockCache.MaxBlocks = h.Cluster.Collections.WebDAVCache.MaxBlockEntries
-}
-
 func (h *handler) serveStatus(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(struct{ Version string }{cmd.Version.String()})
 }
@@ -179,8 +173,6 @@ func (h *handler) Done() <-chan struct{} {
 
 // ServeHTTP implements http.Handler.
 func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
-       h.setupOnce.Do(h.setup)
-
        if xfp := r.Header.Get("X-Forwarded-Proto"); xfp != "" && xfp != "http" {
                r.URL.Scheme = xfp
        }
index 5a12e26e9dcdf75ffd1e3e0b7a90ec7176fbe36e..85c7801cd4cc2a6c6d4be8b4195803bad4b1962b 100644 (file)
@@ -1469,20 +1469,14 @@ func (s *IntegrationSuite) TestFileContentType(c *check.C) {
        }
 }
 
-func (s *IntegrationSuite) TestKeepClientBlockCache(c *check.C) {
-       s.handler.Cluster.Collections.WebDAVCache.MaxBlockEntries = 42
-       c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Not(check.Equals), 42)
-       u := mustParseURL("http://keep-web.example/c=" + arvadostest.FooCollection + "/t=" + arvadostest.ActiveToken + "/foo")
-       req := &http.Request{
-               Method:     "GET",
-               Host:       u.Host,
-               URL:        u,
-               RequestURI: u.RequestURI(),
-       }
+func (s *IntegrationSuite) TestCacheSize(c *check.C) {
+       req, err := http.NewRequest("GET", "http://"+arvadostest.FooCollection+".example.com/foo", nil)
+       req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
+       c.Assert(err, check.IsNil)
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
-       c.Check(resp.Code, check.Equals, http.StatusOK)
-       c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Equals, 42)
+       c.Assert(resp.Code, check.Equals, http.StatusOK)
+       c.Check(s.handler.Cache.sessions[arvadostest.ActiveTokenV2].client.DiskCacheSize.Percent(), check.Equals, int64(10))
 }
 
 // Writing to a collection shouldn't affect its entry in the