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 // Update saves a modified version (fs) to an existing collection
90 // (coll) and, if successful, updates the relevant cache entries so
91 // subsequent calls to Get() reflect the modifications.
92 func (c *cache) Update(client *arvados.Client, coll arvados.Collection, fs arvados.CollectionFileSystem) error {
93 c.setupOnce.Do(c.setup)
95 if m, err := fs.MarshalManifest("."); err != nil || m == coll.ManifestText {
100 var updated arvados.Collection
101 defer c.pdhs.Remove(coll.UUID)
102 err := client.RequestAndDecode(&updated, "PATCH", "/arvados/v1/collections/"+coll.UUID, client.UpdateBody(coll), nil)
104 c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
105 expire: time.Now().Add(time.Duration(c.TTL)),
106 collection: &updated,
112 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
113 c.setupOnce.Do(c.setup)
115 atomic.AddUint64(&c.stats.Requests, 1)
118 permKey := arv.ApiToken + "\000" + targetID
120 } else if ent, cached := c.permissions.Get(permKey); cached {
121 ent := ent.(*cachedPermission)
122 if ent.expire.Before(time.Now()) {
123 c.permissions.Remove(permKey)
126 atomic.AddUint64(&c.stats.PermissionHits, 1)
131 if arvadosclient.PDHMatch(targetID) {
133 } else if ent, cached := c.pdhs.Get(targetID); cached {
134 ent := ent.(*cachedPDH)
135 if ent.expire.Before(time.Now()) {
136 c.pdhs.Remove(targetID)
139 atomic.AddUint64(&c.stats.PDHHits, 1)
143 var collection *arvados.Collection
145 collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
148 if collection != nil && permOK {
149 return collection, nil
150 } else if collection != nil {
151 // Ask API for current PDH for this targetID. Most
152 // likely, the cached PDH is still correct; if so,
153 // _and_ the current token has permission, we can
154 // use our cached manifest.
155 atomic.AddUint64(&c.stats.APICalls, 1)
156 var current arvados.Collection
157 err := arv.Get("collections", targetID, selectPDH, ¤t)
161 if current.PortableDataHash == pdh {
162 c.permissions.Add(permKey, &cachedPermission{
163 expire: time.Now().Add(time.Duration(c.TTL)),
166 c.pdhs.Add(targetID, &cachedPDH{
167 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
171 return collection, err
173 // PDH changed, but now we know we have
174 // permission -- and maybe we already have the
175 // new PDH in the cache.
176 if coll := c.lookupCollection(arv.ApiToken + "\000" + current.PortableDataHash); coll != nil {
182 // Collection manifest is not cached.
183 atomic.AddUint64(&c.stats.APICalls, 1)
184 err := arv.Get("collections", targetID, nil, &collection)
188 exp := time.Now().Add(time.Duration(c.TTL))
189 c.permissions.Add(permKey, &cachedPermission{
192 c.pdhs.Add(targetID, &cachedPDH{
193 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
194 pdh: collection.PortableDataHash,
196 c.collections.Add(arv.ApiToken+"\000"+collection.PortableDataHash, &cachedCollection{
198 collection: collection,
200 if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
201 go c.pruneCollections()
203 return collection, nil
206 // pruneCollections checks the total bytes occupied by manifest_text
207 // in the collection cache and removes old entries as needed to bring
208 // the total size down to CollectionBytes. It also deletes all expired
211 // pruneCollections does not aim to be perfectly correct when there is
212 // concurrent cache activity.
213 func (c *cache) pruneCollections() {
216 keys := c.collections.Keys()
217 entsize := make([]int, len(keys))
218 expired := make([]bool, len(keys))
219 for i, k := range keys {
220 v, ok := c.collections.Peek(k)
224 ent := v.(*cachedCollection)
225 n := len(ent.collection.ManifestText)
228 expired[i] = ent.expire.Before(now)
230 for i, k := range keys {
232 c.collections.Remove(k)
233 size -= int64(entsize[i])
236 for i, k := range keys {
237 if size <= c.MaxCollectionBytes {
241 // already removed this entry in the previous loop
244 c.collections.Remove(k)
245 size -= int64(entsize[i])
249 // collectionBytes returns the approximate memory size of the
251 func (c *cache) collectionBytes() uint64 {
253 for _, k := range c.collections.Keys() {
254 v, ok := c.collections.Peek(k)
258 size += uint64(len(v.(*cachedCollection).collection.ManifestText))
263 func (c *cache) lookupCollection(key string) *arvados.Collection {
264 if ent, cached := c.collections.Get(key); !cached {
267 ent := ent.(*cachedCollection)
268 if ent.expire.Before(time.Now()) {
269 c.collections.Remove(key)
272 atomic.AddUint64(&c.stats.CollectionHits, 1)
273 return ent.collection