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  }