1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
12 "git.curoverse.com/arvados.git/sdk/go/arvados"
13 "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
14 "github.com/hashicorp/golang-lru"
19 MaxCollectionEntries int
20 MaxCollectionBytes int64
21 MaxPermissionEntries int
25 pdhs *lru.TwoQueueCache
26 collections *lru.TwoQueueCache
27 permissions *lru.TwoQueueCache
31 type cacheStats struct {
32 Requests uint64 `json:"Cache.Requests"`
33 CollectionBytes uint64 `json:"Cache.CollectionBytes"`
34 CollectionEntries int `json:"Cache.CollectionEntries"`
35 CollectionHits uint64 `json:"Cache.CollectionHits"`
36 PDHHits uint64 `json:"Cache.UUIDHits"`
37 PermissionHits uint64 `json:"Cache.PermissionHits"`
38 APICalls uint64 `json:"Cache.APICalls"`
41 type cachedPDH struct {
46 type cachedCollection struct {
48 collection *arvados.Collection
51 type cachedPermission struct {
55 func (c *cache) setup() {
57 c.pdhs, err = lru.New2Q(c.MaxUUIDEntries)
61 c.collections, err = lru.New2Q(c.MaxCollectionEntries)
65 c.permissions, err = lru.New2Q(c.MaxPermissionEntries)
71 var selectPDH = map[string]interface{}{
72 "select": []string{"portable_data_hash"},
75 func (c *cache) Stats() cacheStats {
76 c.setupOnce.Do(c.setup)
78 Requests: atomic.LoadUint64(&c.stats.Requests),
79 CollectionBytes: c.collectionBytes(),
80 CollectionEntries: c.collections.Len(),
81 CollectionHits: atomic.LoadUint64(&c.stats.CollectionHits),
82 PDHHits: atomic.LoadUint64(&c.stats.PDHHits),
83 PermissionHits: atomic.LoadUint64(&c.stats.PermissionHits),
84 APICalls: atomic.LoadUint64(&c.stats.APICalls),
88 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
89 c.setupOnce.Do(c.setup)
91 atomic.AddUint64(&c.stats.Requests, 1)
94 permKey := arv.ApiToken + "\000" + targetID
96 } else if ent, cached := c.permissions.Get(permKey); cached {
97 ent := ent.(*cachedPermission)
98 if ent.expire.Before(time.Now()) {
99 c.permissions.Remove(permKey)
102 atomic.AddUint64(&c.stats.PermissionHits, 1)
107 if arvadosclient.PDHMatch(targetID) {
109 } else if ent, cached := c.pdhs.Get(targetID); cached {
110 ent := ent.(*cachedPDH)
111 if ent.expire.Before(time.Now()) {
112 c.pdhs.Remove(targetID)
115 atomic.AddUint64(&c.stats.PDHHits, 1)
119 var collection *arvados.Collection
121 collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
124 if collection != nil && permOK {
125 return collection, nil
126 } else if collection != nil {
127 // Ask API for current PDH for this targetID. Most
128 // likely, the cached PDH is still correct; if so,
129 // _and_ the current token has permission, we can
130 // use our cached manifest.
131 atomic.AddUint64(&c.stats.APICalls, 1)
132 var current arvados.Collection
133 err := arv.Get("collections", targetID, selectPDH, ¤t)
137 if current.PortableDataHash == pdh {
138 exp := time.Now().Add(time.Duration(c.TTL))
139 c.permissions.Add(permKey, &cachedPermission{
143 c.pdhs.Add(targetID, &cachedPDH{
148 return collection, err
150 // PDH changed, but now we know we have
151 // permission -- and maybe we already have the
152 // new PDH in the cache.
153 if coll := c.lookupCollection(arv.ApiToken + "\000" + current.PortableDataHash); coll != nil {
159 // Collection manifest is not cached.
160 atomic.AddUint64(&c.stats.APICalls, 1)
161 err := arv.Get("collections", targetID, nil, &collection)
165 exp := time.Now().Add(time.Duration(c.TTL))
166 c.permissions.Add(permKey, &cachedPermission{
169 c.pdhs.Add(targetID, &cachedPDH{
171 pdh: collection.PortableDataHash,
173 c.collections.Add(arv.ApiToken+"\000"+collection.PortableDataHash, &cachedCollection{
175 collection: collection,
177 if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
178 go c.pruneCollections()
180 return collection, nil
183 // pruneCollections checks the total bytes occupied by manifest_text
184 // in the collection cache and removes old entries as needed to bring
185 // the total size down to CollectionBytes. It also deletes all expired
188 // pruneCollections does not aim to be perfectly correct when there is
189 // concurrent cache activity.
190 func (c *cache) pruneCollections() {
193 keys := c.collections.Keys()
194 entsize := make([]int, len(keys))
195 expired := make([]bool, len(keys))
196 for i, k := range keys {
197 v, ok := c.collections.Peek(k)
201 ent := v.(*cachedCollection)
202 n := len(ent.collection.ManifestText)
205 expired[i] = ent.expire.Before(now)
207 for i, k := range keys {
209 c.collections.Remove(k)
210 size -= int64(entsize[i])
213 for i, k := range keys {
214 if size <= c.MaxCollectionBytes {
218 // already removed this entry in the previous loop
221 c.collections.Remove(k)
222 size -= int64(entsize[i])
226 // collectionBytes returns the approximate memory size of the
228 func (c *cache) collectionBytes() uint64 {
230 for _, k := range c.collections.Keys() {
231 v, ok := c.collections.Peek(k)
235 size += uint64(len(v.(*cachedCollection).collection.ManifestText))
240 func (c *cache) lookupCollection(key string) *arvados.Collection {
241 if ent, cached := c.collections.Get(key); !cached {
244 ent := ent.(*cachedCollection)
245 if ent.expire.Before(time.Now()) {
246 c.collections.Remove(key)
249 atomic.AddUint64(&c.stats.CollectionHits, 1)
250 return ent.collection