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 UUIDTTL arvados.Duration
20 MaxCollectionEntries int
21 MaxCollectionBytes int64
22 MaxPermissionEntries int
26 pdhs *lru.TwoQueueCache
27 collections *lru.TwoQueueCache
28 permissions *lru.TwoQueueCache
32 type cacheStats struct {
33 Requests uint64 `json:"Cache.Requests"`
34 CollectionBytes uint64 `json:"Cache.CollectionBytes"`
35 CollectionEntries int `json:"Cache.CollectionEntries"`
36 CollectionHits uint64 `json:"Cache.CollectionHits"`
37 PDHHits uint64 `json:"Cache.UUIDHits"`
38 PermissionHits uint64 `json:"Cache.PermissionHits"`
39 APICalls uint64 `json:"Cache.APICalls"`
42 type cachedPDH struct {
47 type cachedCollection struct {
49 collection *arvados.Collection
52 type cachedPermission struct {
56 func (c *cache) setup() {
58 c.pdhs, err = lru.New2Q(c.MaxUUIDEntries)
62 c.collections, err = lru.New2Q(c.MaxCollectionEntries)
66 c.permissions, err = lru.New2Q(c.MaxPermissionEntries)
72 var selectPDH = map[string]interface{}{
73 "select": []string{"portable_data_hash"},
76 func (c *cache) Stats() cacheStats {
77 c.setupOnce.Do(c.setup)
79 Requests: atomic.LoadUint64(&c.stats.Requests),
80 CollectionBytes: c.collectionBytes(),
81 CollectionEntries: c.collections.Len(),
82 CollectionHits: atomic.LoadUint64(&c.stats.CollectionHits),
83 PDHHits: atomic.LoadUint64(&c.stats.PDHHits),
84 PermissionHits: atomic.LoadUint64(&c.stats.PermissionHits),
85 APICalls: atomic.LoadUint64(&c.stats.APICalls),
89 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
90 c.setupOnce.Do(c.setup)
92 atomic.AddUint64(&c.stats.Requests, 1)
95 permKey := arv.ApiToken + "\000" + targetID
97 } else if ent, cached := c.permissions.Get(permKey); cached {
98 ent := ent.(*cachedPermission)
99 if ent.expire.Before(time.Now()) {
100 c.permissions.Remove(permKey)
103 atomic.AddUint64(&c.stats.PermissionHits, 1)
108 if arvadosclient.PDHMatch(targetID) {
110 } else if ent, cached := c.pdhs.Get(targetID); cached {
111 ent := ent.(*cachedPDH)
112 if ent.expire.Before(time.Now()) {
113 c.pdhs.Remove(targetID)
116 atomic.AddUint64(&c.stats.PDHHits, 1)
120 var collection *arvados.Collection
122 collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
125 if collection != nil && permOK {
126 return collection, nil
127 } else if collection != nil {
128 // Ask API for current PDH for this targetID. Most
129 // likely, the cached PDH is still correct; if so,
130 // _and_ the current token has permission, we can
131 // use our cached manifest.
132 atomic.AddUint64(&c.stats.APICalls, 1)
133 var current arvados.Collection
134 err := arv.Get("collections", targetID, selectPDH, ¤t)
138 if current.PortableDataHash == pdh {
139 c.permissions.Add(permKey, &cachedPermission{
140 expire: time.Now().Add(time.Duration(c.TTL)),
143 c.pdhs.Add(targetID, &cachedPDH{
144 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
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{
170 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
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