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, client.UpdateBody(coll), nil)
162 c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
163 expire: time.Now().Add(time.Duration(c.TTL)),
164 collection: &updated,
170 func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceReload bool) (*arvados.Collection, error) {
171 c.setupOnce.Do(c.setup)
172 c.metrics.requests.Inc()
175 permKey := arv.ApiToken + "\000" + targetID
177 } else if ent, cached := c.permissions.Get(permKey); cached {
178 ent := ent.(*cachedPermission)
179 if ent.expire.Before(time.Now()) {
180 c.permissions.Remove(permKey)
183 c.metrics.permissionHits.Inc()
188 if arvadosclient.PDHMatch(targetID) {
190 } else if ent, cached := c.pdhs.Get(targetID); cached {
191 ent := ent.(*cachedPDH)
192 if ent.expire.Before(time.Now()) {
193 c.pdhs.Remove(targetID)
196 c.metrics.pdhHits.Inc()
200 var collection *arvados.Collection
202 collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
205 if collection != nil && permOK {
206 return collection, nil
207 } else if collection != nil {
208 // Ask API for current PDH for this targetID. Most
209 // likely, the cached PDH is still correct; if so,
210 // _and_ the current token has permission, we can
211 // use our cached manifest.
212 c.metrics.apiCalls.Inc()
213 var current arvados.Collection
214 err := arv.Get("collections", targetID, selectPDH, ¤t)
218 if current.PortableDataHash == pdh {
219 c.permissions.Add(permKey, &cachedPermission{
220 expire: time.Now().Add(time.Duration(c.TTL)),
223 c.pdhs.Add(targetID, &cachedPDH{
224 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
228 return collection, err
230 // PDH changed, but now we know we have
231 // permission -- and maybe we already have the
232 // new PDH in the cache.
233 if coll := c.lookupCollection(arv.ApiToken + "\000" + current.PortableDataHash); coll != nil {
239 // Collection manifest is not cached.
240 c.metrics.apiCalls.Inc()
241 err := arv.Get("collections", targetID, nil, &collection)
245 exp := time.Now().Add(time.Duration(c.TTL))
246 c.permissions.Add(permKey, &cachedPermission{
249 c.pdhs.Add(targetID, &cachedPDH{
250 expire: time.Now().Add(time.Duration(c.UUIDTTL)),
251 pdh: collection.PortableDataHash,
253 c.collections.Add(arv.ApiToken+"\000"+collection.PortableDataHash, &cachedCollection{
255 collection: collection,
257 if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
258 go c.pruneCollections()
260 return collection, nil
263 // pruneCollections checks the total bytes occupied by manifest_text
264 // in the collection cache and removes old entries as needed to bring
265 // the total size down to CollectionBytes. It also deletes all expired
268 // pruneCollections does not aim to be perfectly correct when there is
269 // concurrent cache activity.
270 func (c *cache) pruneCollections() {
273 keys := c.collections.Keys()
274 entsize := make([]int, len(keys))
275 expired := make([]bool, len(keys))
276 for i, k := range keys {
277 v, ok := c.collections.Peek(k)
281 ent := v.(*cachedCollection)
282 n := len(ent.collection.ManifestText)
285 expired[i] = ent.expire.Before(now)
287 for i, k := range keys {
289 c.collections.Remove(k)
290 size -= int64(entsize[i])
293 for i, k := range keys {
294 if size <= c.MaxCollectionBytes {
298 // already removed this entry in the previous loop
301 c.collections.Remove(k)
302 size -= int64(entsize[i])
306 // collectionBytes returns the approximate memory size of the
308 func (c *cache) collectionBytes() uint64 {
310 for _, k := range c.collections.Keys() {
311 v, ok := c.collections.Peek(k)
315 size += uint64(len(v.(*cachedCollection).collection.ManifestText))
320 func (c *cache) lookupCollection(key string) *arvados.Collection {
321 e, cached := c.collections.Get(key)
325 ent := e.(*cachedCollection)
326 if ent.expire.Before(time.Now()) {
327 c.collections.Remove(key)
330 c.metrics.collectionHits.Inc()
331 return ent.collection