| package atomic_miss_cache |
| |
| /* |
| atomic_miss_cache implements a cache which allows the caller to set a |
| value for a given key only if it is unset, while locking the individual |
| entry. This is convenient when obtaining the value for a key on cache |
| miss is expensive and should only be done if absolutely necessary. |
| */ |
| |
| import ( |
| "context" |
| "errors" |
| "sync" |
| ) |
| |
| var ( |
| // ErrNoSuchEntry is returned when there is no Value for the given key. |
| ErrNoSuchEntry = errors.New("No such entry.") |
| |
| ErrNoNilValue = errors.New("Illegal value; Value cannot be nil.") |
| ) |
| |
| // Value represents a value stored in the cache. |
| type Value interface{} |
| |
| // cacheEntry represents a single entry (ie. a key/value pair) in the cache. |
| // Access is controlled by a mutex to prevent concurrent access, so that the |
| // value can be set if missing. |
| type cacheEntry struct { |
| backingCache ICache |
| mtx sync.Mutex |
| key string |
| val Value |
| } |
| |
| // getLocked returns the stored value for this cacheEntry. If there is no value |
| // in the cache, the backingCache is checked, if it exists. If no value is found |
| // in the backingCache, ErrNoSuchEntry is returned. Assumes that the caller |
| // holds e.mtx. |
| func (e *cacheEntry) getLocked(ctx context.Context) (Value, error) { |
| if e.val == nil { |
| if e.backingCache == nil { |
| return nil, ErrNoSuchEntry |
| } |
| val, err := e.backingCache.Get(ctx, e.key) |
| if err != nil { |
| return nil, err |
| } |
| if val == nil { |
| return nil, ErrNoSuchEntry |
| } |
| e.val = val |
| } |
| return e.val, nil |
| } |
| |
| // Get returns the stored value for this cacheEntry. If there is no value in the |
| // cache, the backingCache is checked, if it exists. If no value is found in the |
| // backingCache, ErrNoSuchEntry is returned. |
| func (e *cacheEntry) Get(ctx context.Context) (Value, error) { |
| e.mtx.Lock() |
| defer e.mtx.Unlock() |
| return e.getLocked(ctx) |
| } |
| |
| // setLocked sets the value for this cacheEntry. Writes through to the |
| // backingCache if it exists. Assumes that the caller holds e.mtx. |
| func (e *cacheEntry) setLocked(ctx context.Context, value Value) error { |
| if value == nil { |
| return ErrNoNilValue |
| } |
| if e.backingCache != nil { |
| if err := e.backingCache.Set(ctx, e.key, value); err != nil { |
| return err |
| } |
| } |
| e.val = value |
| return nil |
| } |
| |
| // Set sets the value for this cacheEntry. Writes through to the backingCache |
| // if it exists. The given Value should not be nil. |
| func (e *cacheEntry) Set(ctx context.Context, value Value) error { |
| e.mtx.Lock() |
| defer e.mtx.Unlock() |
| return e.setLocked(ctx, value) |
| } |
| |
| // SetIfUnset checks for the existence of a value for this cacheEntry, reading |
| // through to the backingCache if it exists and the value is not found in the |
| // cacheEntry. If no value is found, calls getVal to obtain a value to store. If |
| // getVal returns no error, the value is stored in the cacheEntry and is written |
| // through to the backingCache. Returns the existing or new value. getVal should |
| // not return nil with no error. getVal is run with the cacheEntry locked, so it |
| // should not call any methods on cacheEntry, or a deadlock will occur. |
| func (e *cacheEntry) SetIfUnset(ctx context.Context, getVal func(context.Context) (Value, error)) (Value, error) { |
| e.mtx.Lock() |
| defer e.mtx.Unlock() |
| value, err := e.getLocked(ctx) |
| if err != nil && err != ErrNoSuchEntry { |
| return nil, err |
| } else if err == ErrNoSuchEntry { |
| newVal, err := getVal(ctx) |
| if err != nil { |
| return nil, err |
| } |
| if err := e.setLocked(ctx, newVal); err != nil { |
| return nil, err |
| } |
| value = newVal |
| } |
| return value, nil |
| } |
| |
| // ICache is an interface which defines the behaviors of a backing cache. |
| type ICache interface { |
| // Get returns the value for the given key or ErrNoSuchEntry. Get should |
| // not return nil without an error. |
| Get(context.Context, string) (Value, error) |
| |
| // Set sets the value for the given key. |
| Set(context.Context, string, Value) error |
| |
| // Delete deletes the value for the given key. This is used for cleanup |
| // purposes and can be a no-op for backing caches which implement |
| // persistent storage. |
| Delete(context.Context, string) error |
| } |
| |
| // AtomicMissCache implements a cache which allows the caller to set a value for |
| // a given key only if it is unset, while locking the individual entry. This is |
| // convenient when obtaining the value for a key on cache miss is expensive and |
| // should only be done if absolutely necessary. |
| type AtomicMissCache struct { |
| mtx sync.Mutex |
| cache map[string]*cacheEntry |
| backingCache ICache |
| } |
| |
| // New returns a AtomicMissCache instance which uses the given optional |
| // backingCache to read and write through. The returned AtomicMissCache is |
| // goroutine-safe if the given backingCache is goroutine-safe. |
| func New(backingCache ICache) *AtomicMissCache { |
| return &AtomicMissCache{ |
| backingCache: backingCache, |
| cache: map[string]*cacheEntry{}, |
| } |
| } |
| |
| // getEntry is a helper function which retrieves the given entry from the cache, |
| // creating an empty cacheEntry if it does not yet exist. If a new entry is |
| // created but no value is set, it will be removed on the next call to Cleanup. |
| func (c *AtomicMissCache) getEntry(key string) *cacheEntry { |
| c.mtx.Lock() |
| defer c.mtx.Unlock() |
| entry, ok := c.cache[key] |
| if !ok { |
| entry = &cacheEntry{ |
| backingCache: c.backingCache, |
| key: key, |
| } |
| c.cache[key] = entry |
| } |
| return entry |
| } |
| |
| // Get returns the stored value for the given key in the cache. If there is no |
| // value in the cache, the backingCache is checked, if it exists. If no value is |
| // found in the backingCache, ErrNoSuchEntry is returned. |
| func (c *AtomicMissCache) Get(ctx context.Context, key string) (Value, error) { |
| return c.getEntry(key).Get(ctx) |
| } |
| |
| // Set sets the value for the given key in the cache. Writes through to the |
| // backingCache if it exists. The given Value should not be nil. |
| func (c *AtomicMissCache) Set(ctx context.Context, key string, value Value) error { |
| return c.getEntry(key).Set(ctx, value) |
| } |
| |
| // SetIfUnset checks for the existence of a value for the given key, reading |
| // through to the backingCache if it exists and the value is not found in the |
| // cache. If no value is found, calls getVal to obtain a value to store. If |
| // getVal returns no error, the value is stored in the cache and is written |
| // through to the backingCache. Returns the existing or new value. getVal should |
| // not return nil with no error. getVal is run with the cache entry locked, so |
| // it should not attempt to retrieve the same entry, or a deadlock will occur. |
| func (c *AtomicMissCache) SetIfUnset(ctx context.Context, key string, getVal func(ctx context.Context) (Value, error)) (Value, error) { |
| return c.getEntry(key).SetIfUnset(ctx, getVal) |
| } |
| |
| // deleteLocked deletes the value for the given key in the cache. Also deletes |
| // the value in the backingCache, if it exists. Assumes that the caller holds |
| // c.mtx AND the mutex of the cache entry in question, if it exists. |
| func (c *AtomicMissCache) deleteLocked(ctx context.Context, key string) error { |
| if c.backingCache != nil { |
| if err := c.backingCache.Delete(ctx, key); err != nil { |
| return err |
| } |
| } |
| delete(c.cache, key) |
| return nil |
| } |
| |
| // Delete deletes the value for the given key in the cache. Also deletes the |
| // value in the backingCache, if it exists. |
| func (c *AtomicMissCache) Delete(ctx context.Context, key string) error { |
| c.mtx.Lock() |
| defer c.mtx.Unlock() |
| e, ok := c.cache[key] |
| if ok { |
| e.mtx.Lock() |
| defer e.mtx.Unlock() |
| } |
| return c.deleteLocked(ctx, key) |
| } |
| |
| // Cleanup runs the given function over every cache entry. For a given entry, if |
| // the function returns true, the entry is deleted. Also deletes the entry in |
| // the backingCache, if it exists. shouldDelete should not call any methods on |
| // the AtomicMissCache. The cache does not stay locked throughout the Cleanup() |
| // procedure, so shouldDelete should return true only if the entry should be |
| // deleted despite being updated concurrently. |
| func (c *AtomicMissCache) Cleanup(ctx context.Context, shouldDelete func(context.Context, string, Value) bool) error { |
| // Temporarily copy the cache, so that we don't have to have c.mtx |
| // locked while we lock each individual entry. |
| c.mtx.Lock() |
| cache := make(map[string]*cacheEntry, len(c.cache)) |
| for key, entry := range c.cache { |
| cache[key] = entry |
| } |
| c.mtx.Unlock() |
| |
| // Determine which entries need to be deleted. |
| delete := map[string]*cacheEntry{} |
| for key, entry := range cache { |
| entry.mtx.Lock() |
| if entry.val == nil || shouldDelete(ctx, key, entry.val) { |
| // If there is no Value for this entry, then it's left |
| // over from a previous incomplete Get or Set. |
| delete[key] = entry |
| } |
| entry.mtx.Unlock() |
| } |
| |
| // Delete the entries. |
| if len(delete) > 0 { |
| c.mtx.Lock() |
| defer c.mtx.Unlock() |
| for key, entry := range delete { |
| entry.mtx.Lock() |
| if err := c.deleteLocked(ctx, key); err != nil { |
| entry.mtx.Unlock() |
| return err |
| } |
| entry.mtx.Unlock() |
| } |
| } |
| return nil |
| } |
| |
| // ForEach runs the given function over every cache entry. The function should |
| // not call any methods on the AtomicMissCache. Only iterates entries in the |
| // local cache; does not load cached entries from the backingCache. |
| func (c *AtomicMissCache) ForEach(ctx context.Context, fn func(context.Context, string, Value)) { |
| c.mtx.Lock() |
| defer c.mtx.Unlock() |
| for key, entry := range c.cache { |
| entry.mtx.Lock() |
| // We may have entries with no value because we create missing |
| // entries in calls to Get (or calls to Set which do not finish |
| // successfully). The caller is not interested in these phantom |
| // entries. |
| if entry.val != nil { |
| fn(ctx, key, entry.val) |
| } |
| entry.mtx.Unlock() |
| } |
| } |