go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/caching/process.go (about) 1 // Copyright 2017 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 caching 16 17 import ( 18 "context" 19 "errors" 20 "sync/atomic" 21 "time" 22 23 "go.chromium.org/luci/common/data/caching/lazyslot" 24 "go.chromium.org/luci/common/data/caching/lru" 25 ) 26 27 var ( 28 // ErrNoProcessCache is returned by Fetch if the context doesn't have 29 // ProcessCacheData. 30 // 31 // This usually happens in tests. Use WithEmptyProcessCache to prepare the 32 // context. 33 ErrNoProcessCache = errors.New("no process cache is installed in the context, use WithEmptyProcessCache") 34 ) 35 36 type registeredCache struct { 37 // Produces an empty *lru.Cache[...]. Has to return `any` since factories for 38 // different types of caches are all registered in a single registry. 39 factory func() any 40 41 // TODO(vadimsh): Add a name here and start exporting LRU cache sizes as 42 // monitoring metrics. 43 } 44 45 var ( 46 processCacheKey = "server.caching Process Cache" 47 registeredCaches []registeredCache 48 registeredSlots uint32 49 registrationForbidden uint32 50 ) 51 52 func finishInitTime() { 53 atomic.StoreUint32(®istrationForbidden, 1) 54 } 55 56 func checkStillInitTime() { 57 if atomic.LoadUint32(®istrationForbidden) == 1 { 58 // Note: this panic may happen if NewProcessCacheData is called during 59 // init time, before some RegisterLRUCache call. Use NewProcessCacheData 60 // only from main() (or code under main), not in init(). 61 panic("can't call RegisterLRUCache/RegisterCacheSlot after NewProcessCacheData is called") 62 } 63 } 64 65 // LRUHandle is indirect pointer to a registered LRU process cache. 66 // 67 // Grab it via RegisterLRUCache during module init time, and use its LRU() 68 // method to access an actual LRU cache associated with this handle. 69 // 70 // The cache itself lives inside a context. See WithProcessCacheData. 71 type LRUHandle[K comparable, V any] struct{ h int } 72 73 // Valid returns true if h was initialized. 74 func (h LRUHandle[K, V]) Valid() bool { return h.h != 0 } 75 76 // LRU returns global lru.Cache referenced by this handle. 77 // 78 // Returns nil if the context doesn't have ProcessCacheData. 79 func (h LRUHandle[K, V]) LRU(ctx context.Context) *lru.Cache[K, V] { 80 if h.h == 0 { 81 panic("calling LRU on a uninitialized LRUHandle") 82 } 83 pcd, _ := ctx.Value(&processCacheKey).(*ProcessCacheData) 84 if pcd == nil { 85 return nil 86 } 87 return pcd.caches[h.h-1].(*lru.Cache[K, V]) 88 } 89 90 // RegisterLRUCache is used during init time to declare an intent that a package 91 // wants to use a process-global LRU cache of given capacity (or 0 for 92 // unlimited). 93 // 94 // The actual cache itself will be stored in ProcessCacheData inside a context. 95 func RegisterLRUCache[K comparable, V any](capacity int) LRUHandle[K, V] { 96 checkStillInitTime() 97 registeredCaches = append(registeredCaches, registeredCache{ 98 factory: func() any { return lru.New[K, V](capacity) }, 99 }) 100 return LRUHandle[K, V]{len(registeredCaches)} 101 } 102 103 // SlotHandle is indirect pointer to a registered process cache slot. 104 // 105 // Such slot holds one arbitrary value, alongside its expiration time. Useful 106 // for representing global singletons that occasionally need to be refreshed. 107 // 108 // Grab it via RegisterCacheSlot during module init time, and use its Fetch() 109 // method to access the value, potentially refreshing it, if necessary. 110 // 111 // The value itself lives inside a context. See WithProcessCacheData. 112 type SlotHandle struct{ h uint32 } 113 114 // Valid returns true if h was initialized. 115 func (h SlotHandle) Valid() bool { return h.h != 0 } 116 117 // FetchCallback knows how to grab a new value for the cache slot (if prev is 118 // nil) or refresh the known one (if prev is not nil). 119 // 120 // If the returned expiration time is 0, the value is considered non-expirable. 121 // If the returned expiration time is <0, the value will be refetched on the 122 // next access. This is sometimes useful in tests that "freeze" time. 123 type FetchCallback func(prev any) (updated any, exp time.Duration, err error) 124 125 // Fetch returns the cached data, if it is available and fresh, or attempts to 126 // refresh it by calling the given callback. 127 // 128 // Returns ErrNoProcessCache if the context doesn't have ProcessCacheData. 129 func (h SlotHandle) Fetch(ctx context.Context, cb FetchCallback) (any, error) { 130 if h.h == 0 { 131 panic("calling Fetch on a uninitialized SlotHandle") 132 } 133 pcd, _ := ctx.Value(&processCacheKey).(*ProcessCacheData) 134 if pcd == nil { 135 return nil, ErrNoProcessCache 136 } 137 return pcd.slots[h.h-1].Get(ctx, lazyslot.Fetcher(cb)) 138 } 139 140 // RegisterCacheSlot is used during init time to preallocate a place for the 141 // cache global variable. 142 // 143 // The actual cache itself will be stored in ProcessCacheData inside a context. 144 func RegisterCacheSlot() SlotHandle { 145 checkStillInitTime() 146 return SlotHandle{atomic.AddUint32(®isteredSlots, 1)} 147 } 148 149 // ProcessCacheData holds all process-cached data (internally). 150 // 151 // It is opaque to the API users. Use NewProcessCacheData in your main() or 152 // below (i.e. any other place _other_ than init()) to allocate it, then inject 153 // it into the context via WithProcessCacheData, and finally access it through 154 // handles registered during init() time via RegisterLRUCache to get a reference 155 // to an actual lru.Cache. 156 // 157 // Each instance of ProcessCacheData is its own universe of global data. This is 158 // useful in unit tests as replacement for global variables. 159 type ProcessCacheData struct { 160 caches []any // handle => *lru.Cache, never nil once initialized 161 slots []lazyslot.Slot // handle => corresponding slot 162 } 163 164 // NewProcessCacheData allocates and initializes all registered LRU caches. 165 // 166 // It returns a fat stateful object that holds all the cached data. Retain it 167 // and share between requests etc. to actually benefit from the cache. 168 // 169 // NewProcessCacheData must be called after init() time (either in main or 170 // code called from main). 171 func NewProcessCacheData() *ProcessCacheData { 172 // Once NewProcessCacheData is used (after init-time is done), we forbid 173 // registering new caches. All RegisterLRUCache/RegisterCacheSlot calls should 174 // happen during module init time. 175 finishInitTime() 176 d := &ProcessCacheData{ 177 caches: make([]any, len(registeredCaches)), 178 slots: make([]lazyslot.Slot, registeredSlots), 179 } 180 for i, params := range registeredCaches { 181 d.caches[i] = params.factory() 182 } 183 return d 184 } 185 186 // WithEmptyProcessCache installs an empty process-global cache storage into 187 // the context. 188 // 189 // Useful in main() when initializing a root context (used as a basis for all 190 // other contexts) or in unit tests to "reset" the cache state. 191 // 192 // Note that using WithEmptyProcessCache when initializing per-request context 193 // makes no sense, since each request will get its own cache. Instead allocate 194 // the storage cache area via NewProcessCacheData(), retain it in some global 195 // variable and install into per-request context via WithProcessCacheData. 196 func WithEmptyProcessCache(ctx context.Context) context.Context { 197 return WithProcessCacheData(ctx, NewProcessCacheData()) 198 } 199 200 // WithProcessCacheData installs an existing process-global cache storage into 201 // the supplied context. 202 // 203 // It must be allocated via NewProcessCacheData(). 204 func WithProcessCacheData(ctx context.Context, data *ProcessCacheData) context.Context { 205 if data.caches == nil { 206 panic("use NewProcessCacheData to allocate ProcessCacheData") 207 } 208 return context.WithValue(ctx, &processCacheKey, data) 209 }