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
20 config *arvados.WebDAVCacheConfig
21 registry *prometheus.Registry
23 pdhs *lru.TwoQueueCache
24 collections *lru.TwoQueueCache
25 permissions *lru.TwoQueueCache
29 type cacheMetrics struct {
30 requests prometheus.Counter
31 collectionBytes prometheus.Gauge
32 collectionEntries prometheus.Gauge
33 collectionHits prometheus.Counter
34 pdhHits prometheus.Counter
35 permissionHits prometheus.Counter
36 apiCalls prometheus.Counter
39 func (m *cacheMetrics) setup(reg *prometheus.Registry) {
40 m.requests = prometheus.NewCounter(prometheus.CounterOpts{
42 Subsystem: "keepweb_collectioncache",
44 Help: "Number of targetID-to-manifest lookups handled.",
46 reg.MustRegister(m.requests)
47 m.collectionHits = prometheus.NewCounter(prometheus.CounterOpts{
49 Subsystem: "keepweb_collectioncache",
51 Help: "Number of pdh-to-manifest cache hits.",
53 reg.MustRegister(m.collectionHits)
54 m.pdhHits = prometheus.NewCounter(prometheus.CounterOpts{
56 Subsystem: "keepweb_collectioncache",
58 Help: "Number of uuid-to-pdh cache hits.",
60 reg.MustRegister(m.pdhHits)
61 m.permissionHits = prometheus.NewCounter(prometheus.CounterOpts{
63 Subsystem: "keepweb_collectioncache",
64 Name: "permission_hits",
65 Help: "Number of targetID-to-permission cache hits.",
67 reg.MustRegister(m.permissionHits)
68 m.apiCalls = prometheus.NewCounter(prometheus.CounterOpts{
70 Subsystem: "keepweb_collectioncache",
72 Help: "Number of outgoing API calls made by cache.",
74 reg.MustRegister(m.apiCalls)
75 m.collectionBytes = prometheus.NewGauge(prometheus.GaugeOpts{
77 Subsystem: "keepweb_collectioncache",
78 Name: "cached_manifest_bytes",
79 Help: "Total size of all manifests in cache.",
81 reg.MustRegister(m.collectionBytes)
82 m.collectionEntries = prometheus.NewGauge(prometheus.GaugeOpts{
84 Subsystem: "keepweb_collectioncache",
85 Name: "cached_manifests",
86 Help: "Number of manifests in cache.",
88 reg.MustRegister(m.collectionEntries)
91 type cachedPDH struct {
96 type cachedCollection struct {
98 collection *arvados.Collection
101 type cachedPermission struct {
105 func (c *cache) setup() {
107 c.pdhs, err = lru.New2Q(c.config.MaxUUIDEntries)
111 c.collections, err = lru.New2Q(c.config.MaxCollectionEntries)
115 c.permissions, err = lru.New2Q(c.config.MaxPermissionEntries)
122 reg = prometheus.NewRegistry()
126 for range time.Tick(metricsUpdateInterval) {
132 func (c *cache) updateGauges() {
133 c.metrics.collectionBytes.Set(float64(c.collectionBytes()))
134 c.metrics.collectionEntries.Set(float64(c.collections.Len()))
137 var selectPDH = map[string]interface{}{
138 "select": []string{"portable_data_hash"},
141 // Update saves a modified version (fs) to an existing collection
142 // (coll) and, if successful, updates the relevant cache entries so
143 // subsequent calls to Get() reflect the modifications.
144 func (c *cache) Update(client *arvados.Client, coll arvados.Collection, fs arvados.CollectionFileSystem) error {
145 c.setupOnce.Do(c.setup)
147 if m, err := fs.MarshalManifest("."); err != nil || m == coll.ManifestText {
150 coll.ManifestText = m
152 var updated arvados.Collection
153 defer c.pdhs.Remove(coll.UUID)
154 err := client.RequestAndDecode(&updated, "PATCH", "arvados/v1/collections/"+coll.UUID, nil, map[string]interface{}{
155 "collection": map[string]string{
156 "manifest_text": coll.ManifestText,
160 c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
161 expire: time.Now().Add(time.Duration(c.config.TTL)),
162 collection: &updated,
168 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
169 c.setupOnce.Do(c.setup)
170 c.metrics.requests.Inc()
173 permKey := arv.ApiToken + "\000" + targetID
175 } else if ent, cached := c.permissions.Get(permKey); cached {
176 ent := ent.(*cachedPermission)
177 if ent.expire.Before(time.Now()) {
178 c.permissions.Remove(permKey)
181 c.metrics.permissionHits.Inc()
186 if arvadosclient.PDHMatch(targetID) {
188 } else if ent, cached := c.pdhs.Get(targetID); cached {
189 ent := ent.(*cachedPDH)
190 if ent.expire.Before(time.Now()) {
191 c.pdhs.Remove(targetID)
194 c.metrics.pdhHits.Inc()
198 var collection *arvados.Collection
200 collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
203 if collection != nil && permOK {
204 return collection, nil
205 } else if collection != nil {
206 // Ask API for current PDH for this targetID. Most
207 // likely, the cached PDH is still correct; if so,
208 // _and_ the current token has permission, we can
209 // use our cached manifest.
210 c.metrics.apiCalls.Inc()
211 var current arvados.Collection
212 err := arv.Get("collections", targetID, selectPDH, ¤t)
216 if current.PortableDataHash == pdh {
217 c.permissions.Add(permKey, &cachedPermission{
218 expire: time.Now().Add(time.Duration(c.config.TTL)),
221 c.pdhs.Add(targetID, &cachedPDH{
222 expire: time.Now().Add(time.Duration(c.config.UUIDTTL)),
226 return collection, err
228 // PDH changed, but now we know we have
229 // permission -- and maybe we already have the
230 // new PDH in the cache.
231 if coll := c.lookupCollection(arv.ApiToken + "\000" + current.PortableDataHash); coll != nil {
237 // Collection manifest is not cached.
238 c.metrics.apiCalls.Inc()
239 err := arv.Get("collections", targetID, nil, &collection)
243 exp := time.Now().Add(time.Duration(c.config.TTL))
244 c.permissions.Add(permKey, &cachedPermission{
247 c.pdhs.Add(targetID, &cachedPDH{
248 expire: time.Now().Add(time.Duration(c.config.UUIDTTL)),
249 pdh: collection.PortableDataHash,
251 c.collections.Add(arv.ApiToken+"\000"+collection.PortableDataHash, &cachedCollection{
253 collection: collection,
255 if int64(len(collection.ManifestText)) > c.config.MaxCollectionBytes/int64(c.config.MaxCollectionEntries) {
256 go c.pruneCollections()
258 return collection, nil
261 // pruneCollections checks the total bytes occupied by manifest_text
262 // in the collection cache and removes old entries as needed to bring
263 // the total size down to CollectionBytes. It also deletes all expired
266 // pruneCollections does not aim to be perfectly correct when there is
267 // concurrent cache activity.
268 func (c *cache) pruneCollections() {
271 keys := c.collections.Keys()
272 entsize := make([]int, len(keys))
273 expired := make([]bool, len(keys))
274 for i, k := range keys {
275 v, ok := c.collections.Peek(k)
279 ent := v.(*cachedCollection)
280 n := len(ent.collection.ManifestText)
283 expired[i] = ent.expire.Before(now)
285 for i, k := range keys {
287 c.collections.Remove(k)
288 size -= int64(entsize[i])
291 for i, k := range keys {
292 if size <= c.config.MaxCollectionBytes {
296 // already removed this entry in the previous loop
299 c.collections.Remove(k)
300 size -= int64(entsize[i])
304 // collectionBytes returns the approximate memory size of the
306 func (c *cache) collectionBytes() uint64 {
308 for _, k := range c.collections.Keys() {
309 v, ok := c.collections.Peek(k)
313 size += uint64(len(v.(*cachedCollection).collection.ManifestText))
318 func (c *cache) lookupCollection(key string) *arvados.Collection {
319 e, cached := c.collections.Get(key)
323 ent := e.(*cachedCollection)
324 if ent.expire.Before(time.Now()) {
325 c.collections.Remove(key)
328 c.metrics.collectionHits.Inc()
329 return ent.collection