8 "git.curoverse.com/arvados.git/sdk/go/arvados"
9 "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
10 "github.com/hashicorp/golang-lru"
15 MaxCollectionEntries int
16 MaxCollectionBytes int64
17 MaxPermissionEntries int
21 pdhs *lru.TwoQueueCache
22 collections *lru.TwoQueueCache
23 permissions *lru.TwoQueueCache
27 type cacheStats struct {
28 Requests uint64 `json:"Cache.Requests"`
29 CollectionBytes uint64 `json:"Cache.CollectionBytes"`
30 CollectionEntries int `json:"Cache.CollectionEntries"`
31 CollectionHits uint64 `json:"Cache.CollectionHits"`
32 PDHHits uint64 `json:"Cache.UUIDHits"`
33 PermissionHits uint64 `json:"Cache.PermissionHits"`
34 APICalls uint64 `json:"Cache.APICalls"`
37 type cachedPDH struct {
42 type cachedCollection struct {
44 collection *arvados.Collection
47 type cachedPermission struct {
51 func (c *cache) setup() {
53 c.pdhs, err = lru.New2Q(c.MaxUUIDEntries)
57 c.collections, err = lru.New2Q(c.MaxCollectionEntries)
61 c.permissions, err = lru.New2Q(c.MaxPermissionEntries)
67 var selectPDH = map[string]interface{}{
68 "select": []string{"portable_data_hash"},
71 func (c *cache) Stats() cacheStats {
72 c.setupOnce.Do(c.setup)
74 Requests: atomic.LoadUint64(&c.stats.Requests),
75 CollectionBytes: c.collectionBytes(),
76 CollectionEntries: c.collections.Len(),
77 CollectionHits: atomic.LoadUint64(&c.stats.CollectionHits),
78 PDHHits: atomic.LoadUint64(&c.stats.PDHHits),
79 PermissionHits: atomic.LoadUint64(&c.stats.PermissionHits),
80 APICalls: atomic.LoadUint64(&c.stats.APICalls),
84 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
85 c.setupOnce.Do(c.setup)
87 atomic.AddUint64(&c.stats.Requests, 1)
90 permKey := arv.ApiToken + "\000" + targetID
92 } else if ent, cached := c.permissions.Get(permKey); cached {
93 ent := ent.(*cachedPermission)
94 if ent.expire.Before(time.Now()) {
95 c.permissions.Remove(permKey)
98 atomic.AddUint64(&c.stats.PermissionHits, 1)
103 if arvadosclient.PDHMatch(targetID) {
105 } else if ent, cached := c.pdhs.Get(targetID); cached {
106 ent := ent.(*cachedPDH)
107 if ent.expire.Before(time.Now()) {
108 c.pdhs.Remove(targetID)
111 atomic.AddUint64(&c.stats.PDHHits, 1)
115 var collection *arvados.Collection
117 collection = c.lookupCollection(pdh)
120 if collection != nil && permOK {
121 return collection, nil
122 } else if collection != nil {
123 // Ask API for current PDH for this targetID. Most
124 // likely, the cached PDH is still correct; if so,
125 // _and_ the current token has permission, we can
126 // use our cached manifest.
127 atomic.AddUint64(&c.stats.APICalls, 1)
128 var current arvados.Collection
129 err := arv.Get("collections", targetID, selectPDH, ¤t)
133 if current.PortableDataHash == pdh {
134 exp := time.Now().Add(time.Duration(c.TTL))
135 c.permissions.Add(permKey, &cachedPermission{
139 c.pdhs.Add(targetID, &cachedPDH{
144 return collection, err
146 // PDH changed, but now we know we have
147 // permission -- and maybe we already have the
148 // new PDH in the cache.
149 if coll := c.lookupCollection(current.PortableDataHash); coll != nil {
155 // Collection manifest is not cached.
156 atomic.AddUint64(&c.stats.APICalls, 1)
157 err := arv.Get("collections", targetID, nil, &collection)
161 exp := time.Now().Add(time.Duration(c.TTL))
162 c.permissions.Add(permKey, &cachedPermission{
165 c.pdhs.Add(targetID, &cachedPDH{
167 pdh: collection.PortableDataHash,
169 c.collections.Add(collection.PortableDataHash, &cachedCollection{
171 collection: collection,
173 if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
174 go c.pruneCollections()
176 return collection, nil
179 // pruneCollections checks the total bytes occupied by manifest_text
180 // in the collection cache and removes old entries as needed to bring
181 // the total size down to CollectionBytes. It also deletes all expired
184 // pruneCollections does not aim to be perfectly correct when there is
185 // concurrent cache activity.
186 func (c *cache) pruneCollections() {
189 keys := c.collections.Keys()
190 entsize := make([]int, len(keys))
191 expired := make([]bool, len(keys))
192 for i, k := range keys {
193 v, ok := c.collections.Peek(k)
197 ent := v.(*cachedCollection)
198 n := len(ent.collection.ManifestText)
201 expired[i] = ent.expire.Before(now)
203 for i, k := range keys {
205 c.collections.Remove(k)
206 size -= int64(entsize[i])
209 for i, k := range keys {
210 if size <= c.MaxCollectionBytes {
214 // already removed this entry in the previous loop
217 c.collections.Remove(k)
218 size -= int64(entsize[i])
222 // collectionBytes returns the approximate memory size of the
224 func (c *cache) collectionBytes() uint64 {
226 for _, k := range c.collections.Keys() {
227 v, ok := c.collections.Peek(k)
231 size += uint64(len(v.(*cachedCollection).collection.ManifestText))
236 func (c *cache) lookupCollection(pdh string) *arvados.Collection {
239 } else if ent, cached := c.collections.Get(pdh); !cached {
242 ent := ent.(*cachedCollection)
243 if ent.expire.Before(time.Now()) {
244 c.collections.Remove(pdh)
247 atomic.AddUint64(&c.stats.CollectionHits, 1)
248 return ent.collection