github.com/creachadair/ffs@v0.17.3/storage/cachestore/cachestore.go (about) 1 // Copyright 2020 Michael J. Fromberger. All Rights Reserved. 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 cachestore implements a [blob.Store] that wraps the keyspaces of an 16 // underlying store in memory caches. 17 package cachestore 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "iter" 24 "strings" 25 "sync" 26 "sync/atomic" 27 28 "github.com/creachadair/ffs/blob" 29 "github.com/creachadair/ffs/storage/dbkey" 30 "github.com/creachadair/ffs/storage/monitor" 31 "github.com/creachadair/mds/cache" 32 "github.com/creachadair/mds/stree" 33 "github.com/creachadair/msync/throttle" 34 "github.com/creachadair/taskgroup" 35 ) 36 37 // Store implements the [blob.StoreCloser] interface. 38 type Store struct { 39 *monitor.M[state, *KV] 40 } 41 42 type state struct { 43 base blob.Store 44 maxBytes int 45 } 46 47 // New constructs a new root Store delegated to base. 48 // It will panic if maxBytes < 0. 49 func New(base blob.Store, maxBytes int) Store { 50 if maxBytes < 0 { 51 panic("cache size is negative") 52 } 53 return Store{M: monitor.New(monitor.Config[state, *KV]{ 54 DB: state{base: base, maxBytes: maxBytes}, 55 NewKV: func(ctx context.Context, db state, _ dbkey.Prefix, name string) (*KV, error) { 56 kv, err := db.base.KV(ctx, name) 57 if err != nil { 58 return nil, err 59 } 60 return NewKV(kv, db.maxBytes), nil 61 }, 62 NewSub: func(ctx context.Context, db state, _ dbkey.Prefix, name string) (state, error) { 63 sub, err := db.base.Sub(ctx, name) 64 if err != nil { 65 return state{}, err 66 } 67 return state{base: sub, maxBytes: db.maxBytes}, nil 68 }, 69 })} 70 } 71 72 // Close implements a method of the [blob.StoreCloser] interface. 73 func (s Store) Close(ctx context.Context) error { 74 if c, ok := s.M.DB.base.(blob.Closer); ok { 75 return c.Close(ctx) 76 } 77 return nil 78 } 79 80 // KV implements a [blob.KV] that delegates to an underlying store through an 81 // in-memory cache. This is appropriate for a high-latency or quota-limited 82 // remote store (such as a GCS or S3 bucket) that will not be concurrently 83 // written by other processes; concurrent readers are fine. 84 // 85 // Both reads and writes are cached, and the store writes through to the 86 // underlying store. Negative hits from Get and Size are also cached. 87 type KV struct { 88 base blob.KV 89 90 listed atomic.Bool // keymap has been fully populated 91 92 cache *cache.Cache[string, []byte] // blob cache 93 init throttle.Throttle[any] // populate key map 94 get throttle.Set[string, []byte] // get key (data) 95 put throttle.Set[string, any] // put key (error only) 96 del throttle.Set[string, any] // delete key (error only) 97 98 μ sync.RWMutex // protects the keymap 99 keymap *stree.Tree[string] // known keys 100 101 // The keymap is initialized to the keyspace of the underlying store. 102 // Additional keys are added by store queries. 103 } 104 105 // NewKV constructs a new cached [KV] with the specified capacity in bytes, 106 // delegating storage operations to s. It will panic if maxBytes < 0. 107 func NewKV(s blob.KV, maxBytes int) *KV { 108 return &KV{ 109 base: s, 110 cache: cache.New(cache.LRU[string, []byte](int64(maxBytes)). 111 WithSize(cache.Length), 112 ), 113 } 114 } 115 116 // Get implements a method of [blob.KV]. 117 func (s *KV) Get(ctx context.Context, key string) ([]byte, error) { 118 if err := s.initKeyMap(ctx); err != nil { 119 return nil, err 120 } 121 122 data, cached, err := s.getLocal(ctx, key) 123 if err != nil { 124 return nil, err 125 } else if cached { 126 return bytes.Clone(data), nil 127 } 128 129 // Reaching here, the key is in the keymap but not in the cache, so we have 130 // to fetch it from the underlying store. 131 return s.get.Call(ctx, key, func(ctx context.Context) ([]byte, error) { 132 data, err := s.base.Get(ctx, key) 133 if err != nil { 134 return nil, err 135 136 // This shouldn't be able to fail, but it is possible the store was 137 // modified out of band, so in that case just don't cache the key. 138 } 139 s.cache.Put(key, data) 140 return data, nil 141 }) 142 } 143 144 // getLocal reports whether key is present in the store, and if so whether 145 // its contents are cached locally. 146 // 147 // If key is not present, it returns nil, false, ErrKeyNotFound. 148 // IF key is present but not cached, it returns nil, false, nil. 149 // If key is present and cached, it returns data, true, nil. 150 // 151 // Precondition: initKeyMap must have previously succeeded. 152 func (s *KV) getLocal(ctx context.Context, key string) ([]byte, bool, error) { 153 s.μ.Lock() 154 defer s.μ.Unlock() 155 if _, ok := s.keymap.Get(key); !ok { 156 return nil, false, blob.KeyNotFound(key) 157 } 158 if data, ok := s.cache.Get(key); ok { 159 return data, true, nil 160 } 161 return nil, false, nil 162 } 163 164 // Has implements a method of [blob.KV]. 165 func (s *KV) Has(ctx context.Context, keys ...string) (blob.KeySet, error) { 166 if err := s.initKeyMap(ctx); err != nil { 167 return nil, err 168 } 169 s.μ.RLock() 170 defer s.μ.RUnlock() 171 var out blob.KeySet 172 for _, key := range keys { 173 if _, ok := s.keymap.Get(key); ok { 174 out.Add(key) 175 } 176 } 177 return out, nil 178 } 179 180 // Put implements a method of [blob.KV]. 181 func (s *KV) Put(ctx context.Context, opts blob.PutOptions) error { 182 if err := s.initKeyMap(ctx); err != nil { 183 return err 184 } 185 if !opts.Replace { 186 s.μ.RLock() 187 _, ok := s.keymap.Get(opts.Key) 188 s.μ.RUnlock() 189 if ok { 190 return blob.KeyExists(opts.Key) 191 } 192 } 193 _, err := s.put.Call(ctx, opts.Key, func(ctx context.Context) (any, error) { 194 if err := s.base.Put(ctx, opts); err != nil { 195 return nil, err 196 } 197 s.μ.Lock() 198 s.keymap.Replace(opts.Key) 199 s.μ.Unlock() 200 s.cache.Put(opts.Key, opts.Data) 201 return nil, nil 202 }) 203 return err 204 } 205 206 // Delete implements a method of [blob.KV]. 207 func (s *KV) Delete(ctx context.Context, key string) error { 208 if err := s.initKeyMap(ctx); err != nil { 209 return err 210 } 211 _, err := s.del.Call(ctx, key, func(ctx context.Context) (any, error) { 212 // Even if we fail to delete the key from the underlying store, take this as 213 // a signal that we should forget about its data. Don't remove it from the 214 // keymap, however, unless the deletion actually succeeds. 215 s.cache.Remove(key) 216 err := s.base.Delete(ctx, key) 217 218 if err == nil || errors.Is(err, blob.ErrKeyNotFound) { 219 s.μ.Lock() 220 defer s.μ.Unlock() 221 s.keymap.Remove(key) 222 } 223 return nil, err 224 }) 225 return err 226 } 227 228 // initKeyMap initializes the key map from the base store. 229 func (s *KV) initKeyMap(ctx context.Context) error { 230 if s.listed.Load() { 231 return nil // affirmatively already done 232 } 233 _, err := s.init.Call(ctx, s.loadKeyMap) 234 return err 235 } 236 237 func (s *KV) loadKeyMap(ctx context.Context) (any, error) { 238 if s.listed.Load() { 239 return nil, nil 240 } 241 var g taskgroup.Group 242 243 // The keymap is not safe for concurrent use by multiple goroutines, so 244 // serialize insertions through a collector. 245 keymap := stree.New[string](300, strings.Compare) 246 coll := taskgroup.Gather(g.Go, func(key string) { 247 keymap.Add(key) 248 }) 249 250 for i := range 256 { 251 if ctx.Err() != nil { 252 break 253 } 254 pfx := string([]byte{byte(i)}) 255 coll.Report(func(report func(string)) error { 256 for key, err := range s.base.List(ctx, pfx) { 257 if err != nil { 258 return err 259 } else if !strings.HasPrefix(key, pfx) { 260 break 261 } 262 report(key) 263 } 264 return nil 265 }) 266 } 267 if err := g.Wait(); err != nil { 268 return nil, err 269 } 270 s.μ.Lock() 271 s.keymap = keymap 272 s.μ.Unlock() 273 s.listed.Store(true) 274 return nil, nil 275 } 276 277 func (s *KV) firstKey(start string) (string, bool) { 278 s.μ.RLock() 279 defer s.μ.RUnlock() 280 return s.keymap.GetNearest(start) 281 } 282 283 func (s *KV) nextKey(prev string) (string, bool) { 284 s.μ.RLock() 285 defer s.μ.RUnlock() 286 return s.keymap.GetNext(prev) 287 } 288 289 // List implements a method of [blob.KV]. 290 func (s *KV) List(ctx context.Context, start string) iter.Seq2[string, error] { 291 return func(yield func(string, error) bool) { 292 if err := s.initKeyMap(ctx); err != nil { 293 yield("", err) 294 return 295 } 296 cur, ok := s.firstKey(start) 297 for ok { 298 if !yield(cur, nil) { 299 return 300 } 301 cur, ok = s.nextKey(cur) 302 } 303 } 304 } 305 306 // Len implements a method of [blob.KV]. 307 func (s *KV) Len(ctx context.Context) (int64, error) { 308 if err := s.initKeyMap(ctx); err != nil { 309 return 0, err 310 } 311 s.μ.RLock() 312 defer s.μ.RUnlock() 313 return int64(s.keymap.Len()), nil 314 }