1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
11 "git.curoverse.com/arvados.git/sdk/go/arvados"
12 "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
13 "github.com/hashicorp/golang-lru"
14 "github.com/prometheus/client_golang/prometheus"
17 const metricsUpdateInterval = time.Second / 10
21 UUIDTTL arvados.Duration
22 MaxCollectionEntries int
23 MaxCollectionBytes int64
24 MaxPermissionEntries int
27 registry *prometheus.Registry
29 pdhs *lru.TwoQueueCache
30 collections *lru.TwoQueueCache
31 permissions *lru.TwoQueueCache
35 type cacheMetrics struct {
36 requests prometheus.Counter
37 collectionBytes prometheus.Gauge
38 collectionEntries prometheus.Gauge
39 collectionHits prometheus.Counter
40 pdhHits prometheus.Counter
41 permissionHits prometheus.Counter
42 apiCalls prometheus.Counter
45 func (m *cacheMetrics) setup(reg *prometheus.Registry) {
46 m.requests = prometheus.NewCounter(prometheus.CounterOpts{
48 Subsystem: "keepweb_collectioncache",
50 Help: "Number of targetID-to-manifest lookups handled.",
52 reg.MustRegister(m.requests)
53 m.collectionHits = prometheus.NewCounter(prometheus.CounterOpts{
55 Subsystem: "keepweb_collectioncache",
57 Help: "Number of pdh-to-manifest cache hits.",
59 reg.MustRegister(m.collectionHits)
60 m.pdhHits = prometheus.NewCounter(prometheus.CounterOpts{
62 Subsystem: "keepweb_collectioncache",
64 Help: "Number of uuid-to-pdh cache hits.",
66 reg.MustRegister(m.pdhHits)
67 m.permissionHits = prometheus.NewCounter(prometheus.CounterOpts{
69 Subsystem: "keepweb_collectioncache",
70 Name: "permission_hits",
71 Help: "Number of targetID-to-permission cache hits.",
73 reg.MustRegister(m.permissionHits)
74 m.apiCalls = prometheus.NewCounter(prometheus.CounterOpts{
76 Subsystem: "keepweb_collectioncache",
78 Help: "Number of outgoing API calls made by cache.",
80 reg.MustRegister(m.apiCalls)
81 m.collectionBytes = prometheus.NewGauge(prometheus.GaugeOpts{
83 Subsystem: "keepweb_collectioncache",
84 Name: "cached_manifest_bytes",
85 Help: "Total size of all manifests in cache.",
87 reg.MustRegister(m.collectionBytes)
88 m.collectionEntries = prometheus.NewGauge(prometheus.GaugeOpts{
90 Subsystem: "keepweb_collectioncache",
91 Name: "cached_manifests",
92 Help: "Number of manifests in cache.",
94 reg.MustRegister(m.collectionEntries)
97 type cachedPDH struct {
102 type cachedCollection struct {
104 collection *arvados.Collection
107 type cachedPermission struct {
111 func (c *cache) setup() {
113 c.pdhs, err = lru.New2Q(c.MaxUUIDEntries)
117 c.collections, err = lru.New2Q(c.MaxCollectionEntries)
121 c.permissions, err = lru.New2Q(c.MaxPermissionEntries)
128 reg = prometheus.NewRegistry()
132 for range time.Tick(metricsUpdateInterval) {
138 func (c *cache) updateGauges() {
139 c.metrics.collectionBytes.Set(float64(c.collectionBytes()))
140 c.metrics.collectionEntries.Set(float64(c.collections.Len()))
143 var selectPDH = map[string]interface{}{
144 "select": []string{"portable_data_hash"},
147 // Update saves a modified version (fs) to an existing collection
148 // (coll) and, if successful, updates the relevant cache entries so
149 // subsequent calls to Get() reflect the modifications.
150 func (c *cache) Update(client *arvados.Client, coll arvados.Collection, fs arvados.CollectionFileSystem) error {
151 c.setupOnce.Do(c.setup)
153 if m, err := fs.MarshalManifest("."); err != nil || m == coll.ManifestText {
156 coll.ManifestText = m
158 var updated arvados.Collection
159 defer c.pdhs.Remove(coll.UUID)
160 err := client.RequestAndDecode(&updated, "PATCH", "arvados/v1/collections/"+coll.UUID, nil, map[string]interface{}{
161 "collection": map[string]string{
162 "manifest_text": coll.ManifestText,
166 c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
167 expire: time.Now().Add(time.Duration(c.TTL)),
168 collection: &updated,
174 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
175 c.setupOnce.Do(c.setup)
176 c.metrics.requests.Inc()
179 permKey := arv.ApiToken + "\000" + targetID
181 } else if ent, cached := c.permissions.Get(permKey); cached {
182 ent := ent.(*cachedPermission)
183 if ent.expire.Before(time.Now()) {
184 c.permissions.Remove(permKey)
187 c.metrics.permissionHits.Inc()
192 if arvadosclient.PDHMatch(targetID) {
194 } else if ent, cached := c.pdhs.Get(targetID); cached {
195 ent := ent.(*cachedPDH)
196 if ent.expire.Before(time.Now()) {
197 c.pdhs.Remove(targetID)
200 c.metrics.pdhHits.Inc()
204 var collection *arvados.Collection
206 collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
209 if collection != nil && permOK {
210 return collection, nil
211 } else if collection != nil {
212 // Ask API for current PDH for this targetID. Most
213 // likely, the cached PDH is still correct; if so,
214 // _and_ the current token has permission, we can
215 // use our cached manifest.
216 c.metrics.apiCalls.Inc()
217 var current arvados.Collection
218 err := arv.Get("collections", targetID, selectPDH, ¤t)
222 if current.PortableDataHash == pdh {
223 c.permissions.Add(permKey, &cachedPermission{
224 expire: time.Now().Add(time.Duration(c.TTL)),
227 c.pdhs.Add(targetID, &cachedPDH{
228 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
232 return collection, err
234 // PDH changed, but now we know we have
235 // permission -- and maybe we already have the
236 // new PDH in the cache.
237 if coll := c.lookupCollection(arv.ApiToken + "\000" + current.PortableDataHash); coll != nil {
243 // Collection manifest is not cached.
244 c.metrics.apiCalls.Inc()
245 err := arv.Get("collections", targetID, nil, &collection)
249 exp := time.Now().Add(time.Duration(c.TTL))
250 c.permissions.Add(permKey, &cachedPermission{
253 c.pdhs.Add(targetID, &cachedPDH{
254 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
255 pdh: collection.PortableDataHash,
257 c.collections.Add(arv.ApiToken+"\000"+collection.PortableDataHash, &cachedCollection{
259 collection: collection,
261 if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
262 go c.pruneCollections()
264 return collection, nil
267 // pruneCollections checks the total bytes occupied by manifest_text
268 // in the collection cache and removes old entries as needed to bring
269 // the total size down to CollectionBytes. It also deletes all expired
272 // pruneCollections does not aim to be perfectly correct when there is
273 // concurrent cache activity.
274 func (c *cache) pruneCollections() {
277 keys := c.collections.Keys()
278 entsize := make([]int, len(keys))
279 expired := make([]bool, len(keys))
280 for i, k := range keys {
281 v, ok := c.collections.Peek(k)
285 ent := v.(*cachedCollection)
286 n := len(ent.collection.ManifestText)
289 expired[i] = ent.expire.Before(now)
291 for i, k := range keys {
293 c.collections.Remove(k)
294 size -= int64(entsize[i])
297 for i, k := range keys {
298 if size <= c.MaxCollectionBytes {
302 // already removed this entry in the previous loop
305 c.collections.Remove(k)
306 size -= int64(entsize[i])
310 // collectionBytes returns the approximate memory size of the
312 func (c *cache) collectionBytes() uint64 {
314 for _, k := range c.collections.Keys() {
315 v, ok := c.collections.Peek(k)
319 size += uint64(len(v.(*cachedCollection).collection.ManifestText))
324 func (c *cache) lookupCollection(key string) *arvados.Collection {
325 e, cached := c.collections.Get(key)
329 ent := e.(*cachedCollection)
330 if ent.expire.Before(time.Now()) {
331 c.collections.Remove(key)
334 c.metrics.collectionHits.Inc()
335 return ent.collection