go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/ui/cursors.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ui 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/base64" 21 "time" 22 23 "go.chromium.org/luci/common/logging" 24 "go.chromium.org/luci/server/caching" 25 ) 26 27 type cursorKind string 28 29 const instancesListing cursorKind = "v1:instances" 30 31 // cursorKey is a cache key for items that link cursor to a previous page. 32 func (k cursorKind) cursorKey(pkg, cursor string) string { 33 blob := sha256.Sum224([]byte(pkg + ":" + cursor)) 34 return string(k) + ":" + base64.RawStdEncoding.EncodeToString(blob[:]) 35 } 36 37 // storePrevCursor stores mapping cursor => prev, so that prev can be fetched 38 // later by fetchPrevCursor. 39 // 40 // Logs and ignores errors. Cursor mapping is non-essential functionality. 41 func (k cursorKind) storePrevCursor(ctx context.Context, pkg, cursor, prev string) { 42 if err := storeInCache(ctx, k.cursorKey(pkg, cursor), []byte(prev)); err != nil { 43 logging.Errorf(ctx, "Failed to store prev cursor %q in the cache: %s", k, err) 44 } 45 } 46 47 // fetchPrevCursor returns a cursor stored by storePrevCursor. 48 // 49 // Logs and ignores errors. Cursor mapping is non-essential functionality. 50 func (k cursorKind) fetchPrevCursor(ctx context.Context, pkg, cursor string) string { 51 blob, err := loadFromCache(ctx, k.cursorKey(pkg, cursor)) 52 if err != nil { 53 logging.Errorf(ctx, "Failed to fetch the prev cursor %q from the cache: %s", k, err) 54 return "" 55 } 56 return string(blob) 57 } 58 59 // localCursorCache is used as a replacement for the global cache when running 60 // locally during development. 61 var localCursorCache = caching.RegisterLRUCache[string, string](1) 62 63 // storeInCache puts the value in the global memory cache. 64 // 65 // If there's no global cache available (e.g. when running locally) uses the 66 // process cache. 67 func storeInCache(ctx context.Context, key string, blob []byte) error { 68 if global := caching.GlobalCache(ctx, "cursors"); global != nil { 69 return global.Set(ctx, key, blob, 24*time.Hour) 70 } 71 localCursorCache.LRU(ctx).Put(ctx, key, string(blob), time.Hour) 72 return nil 73 } 74 75 // loadFromCache loads the value previously stored with storeInCache. 76 // 77 // If there's no such value, returns (nil, nil). 78 func loadFromCache(ctx context.Context, key string) ([]byte, error) { 79 if global := caching.GlobalCache(ctx, "cursors"); global != nil { 80 val, err := global.Get(ctx, key) 81 if err == caching.ErrCacheMiss { 82 return nil, nil 83 } 84 return val, err 85 } 86 if val, ok := localCursorCache.LRU(ctx).Get(ctx, key); ok { 87 return []byte(val), nil 88 } 89 return nil, nil 90 }