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(&registrationForbidden, 1)
    54  }
    55  
    56  func checkStillInitTime() {
    57  	if atomic.LoadUint32(&registrationForbidden) == 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(&registeredSlots, 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  }