* MM-30863: Fix race in LRU After shortening the critical section, we missed out the fact that the byte slice is still accessible after the element is returned. So the lock needs to be active until the byte slice is fully read and unmarshaled https://mattermost.atlassian.net/browse/MM-30863 ```release-note NONE ``` * incorporate suggestions Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
254 строки
6.3 KiB
Go
254 строки
6.3 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package cache
|
|
|
|
import (
|
|
"container/list"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
"github.com/tinylib/msgp/msgp"
|
|
"github.com/vmihailenco/msgpack/v5"
|
|
)
|
|
|
|
// LRU is a thread-safe fixed size LRU cache.
|
|
type LRU struct {
|
|
name string
|
|
size int
|
|
evictList *list.List
|
|
items map[string]*list.Element
|
|
lock sync.RWMutex
|
|
defaultExpiry time.Duration
|
|
invalidateClusterEvent string
|
|
currentGeneration int64
|
|
len int
|
|
}
|
|
|
|
// LRUOptions contains options for initializing LRU cache
|
|
type LRUOptions struct {
|
|
Name string
|
|
Size int
|
|
DefaultExpiry time.Duration
|
|
InvalidateClusterEvent string
|
|
}
|
|
|
|
// entry is used to hold a value in the evictList.
|
|
type entry struct {
|
|
key string
|
|
value []byte
|
|
expires time.Time
|
|
generation int64
|
|
}
|
|
|
|
// NewLRU creates an LRU of the given size.
|
|
func NewLRU(opts *LRUOptions) Cache {
|
|
return &LRU{
|
|
name: opts.Name,
|
|
size: opts.Size,
|
|
evictList: list.New(),
|
|
items: make(map[string]*list.Element, opts.Size),
|
|
defaultExpiry: opts.DefaultExpiry,
|
|
invalidateClusterEvent: opts.InvalidateClusterEvent,
|
|
}
|
|
}
|
|
|
|
// Purge is used to completely clear the cache.
|
|
func (l *LRU) Purge() error {
|
|
l.lock.Lock()
|
|
defer l.lock.Unlock()
|
|
|
|
l.len = 0
|
|
l.currentGeneration++
|
|
return nil
|
|
}
|
|
|
|
// Set adds the given key and value to the store without an expiry. If the key already exists,
|
|
// it will overwrite the previous value.
|
|
func (l *LRU) Set(key string, value interface{}) error {
|
|
return l.SetWithExpiry(key, value, 0)
|
|
}
|
|
|
|
// SetWithDefaultExpiry adds the given key and value to the store with the default expiry. If
|
|
// the key already exists, it will overwrite the previoous value
|
|
func (l *LRU) SetWithDefaultExpiry(key string, value interface{}) error {
|
|
return l.SetWithExpiry(key, value, l.defaultExpiry)
|
|
}
|
|
|
|
// SetWithExpiry adds the given key and value to the cache with the given expiry. If the key
|
|
// already exists, it will overwrite the previoous value
|
|
func (l *LRU) SetWithExpiry(key string, value interface{}, ttl time.Duration) error {
|
|
return l.set(key, value, ttl)
|
|
}
|
|
|
|
// Get the content stored in the cache for the given key, and decode it into the value interface.
|
|
// return ErrKeyNotFound if the key is missing from the cache
|
|
func (l *LRU) Get(key string, value interface{}) error {
|
|
return l.get(key, value)
|
|
}
|
|
|
|
// Remove deletes the value for a key.
|
|
func (l *LRU) Remove(key string) error {
|
|
l.lock.Lock()
|
|
defer l.lock.Unlock()
|
|
|
|
if ent, ok := l.items[key]; ok {
|
|
l.removeElement(ent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Keys returns a slice of the keys in the cache.
|
|
func (l *LRU) Keys() ([]string, error) {
|
|
l.lock.RLock()
|
|
defer l.lock.RUnlock()
|
|
|
|
keys := make([]string, l.len)
|
|
i := 0
|
|
for ent := l.evictList.Back(); ent != nil; ent = ent.Prev() {
|
|
e := ent.Value.(*entry)
|
|
if e.generation == l.currentGeneration {
|
|
keys[i] = e.key
|
|
i++
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// Len returns the number of items in the cache.
|
|
func (l *LRU) Len() (int, error) {
|
|
l.lock.RLock()
|
|
defer l.lock.RUnlock()
|
|
return l.len, nil
|
|
}
|
|
|
|
// GetInvalidateClusterEvent returns the cluster event configured when this cache was created.
|
|
func (l *LRU) GetInvalidateClusterEvent() string {
|
|
return l.invalidateClusterEvent
|
|
}
|
|
|
|
// Name returns the name of the cache
|
|
func (l *LRU) Name() string {
|
|
return l.name
|
|
}
|
|
|
|
func (l *LRU) set(key string, value interface{}, ttl time.Duration) error {
|
|
var expires time.Time
|
|
if ttl > 0 {
|
|
expires = time.Now().Add(ttl)
|
|
}
|
|
|
|
var buf []byte
|
|
var err error
|
|
|
|
// We use a fast path for hot structs.
|
|
if msgpVal, ok := value.(msgp.Marshaler); ok {
|
|
buf, err = msgpVal.MarshalMsg(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Slow path for other structs.
|
|
buf, err = msgpack.Marshal(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
l.lock.Lock()
|
|
defer l.lock.Unlock()
|
|
|
|
// Check for existing item, ignoring expiry since we'd update anyway.
|
|
if ent, ok := l.items[key]; ok {
|
|
l.evictList.MoveToFront(ent)
|
|
e := ent.Value.(*entry)
|
|
e.value = buf
|
|
e.expires = expires
|
|
if e.generation != l.currentGeneration {
|
|
e.generation = l.currentGeneration
|
|
l.len++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add new item
|
|
ent := &entry{key, buf, expires, l.currentGeneration}
|
|
entry := l.evictList.PushFront(ent)
|
|
l.items[key] = entry
|
|
l.len++
|
|
|
|
if l.evictList.Len() > l.size {
|
|
l.removeElement(l.evictList.Back())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *LRU) get(key string, value interface{}) error {
|
|
val, err := l.getItem(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We use a fast path for hot structs.
|
|
if msgpVal, ok := value.(msgp.Unmarshaler); ok {
|
|
_, err := msgpVal.UnmarshalMsg(val)
|
|
return err
|
|
}
|
|
|
|
// This is ugly and makes the cache package aware of the model package.
|
|
// But this is due to 2 things.
|
|
// 1. The msgp package works on methods on structs rather than functions.
|
|
// 2. Our cache interface passes pointers to empty pointers, and not pointers
|
|
// to values. This is mainly how all our model structs are passed around.
|
|
// It might be technically possible to use values _just_ for hot structs
|
|
// like these and then return a pointer while returning from the cache function,
|
|
// but it will make the codebase inconsistent, and has some edge-cases to take care of.
|
|
switch v := value.(type) {
|
|
case **model.User:
|
|
var u model.User
|
|
_, err := u.UnmarshalMsg(val)
|
|
*v = &u
|
|
return err
|
|
case **model.Session:
|
|
var s model.Session
|
|
_, err := s.UnmarshalMsg(val)
|
|
*v = &s
|
|
return err
|
|
case *map[string]*model.User:
|
|
var u model.UserMap
|
|
_, err := u.UnmarshalMsg(val)
|
|
*v = u
|
|
return err
|
|
}
|
|
|
|
// Slow path for other structs.
|
|
return msgpack.Unmarshal(val, value)
|
|
}
|
|
|
|
func (l *LRU) getItem(key string) ([]byte, error) {
|
|
l.lock.Lock()
|
|
defer l.lock.Unlock()
|
|
|
|
ent, ok := l.items[key]
|
|
if !ok {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
e := ent.Value.(*entry)
|
|
if e.generation != l.currentGeneration || (!e.expires.IsZero() && time.Now().After(e.expires)) {
|
|
l.removeElement(ent)
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
l.evictList.MoveToFront(ent)
|
|
return e.value, nil
|
|
}
|
|
|
|
func (l *LRU) removeElement(e *list.Element) {
|
|
l.evictList.Remove(e)
|
|
kv := e.Value.(*entry)
|
|
if kv.generation == l.currentGeneration {
|
|
l.len--
|
|
}
|
|
delete(l.items, kv.key)
|
|
}
|