MM-59932: Migrate remaining caches to Redis (#27880)
- We introduce 2 new APIs: 1. Scan: this allows incremental iteration without blocking the Redis server and is the recommended way to iterate over keys. With this, we have entirely removed the need for Keys. 2. RemoveMulti: this allows deletion of multiple keys in a single operation which optimizes network round trips. - While here, we make a small improvement to GetStatusFromCache, where we remove the shallow copy which wasn't necessary because we always serialize the data from the cache. - We do not use Redis for session cache because of frequent requests to iterate the entire cache which leads to a lot of `SCAN` calls. - Avoid broadcasting status update messages for Redis case. - Setting cache expiry for status cache - Removing .Set method altogether to prevent any chances of setting an item with no expiry. https://mattermost.atlassian.net/browse/MM-59932 ```release-note NONE ```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e5842e67a8
Коммит
a1012d33eb
18
server/platform/services/cache/cache.go
поставляемый
18
server/platform/services/cache/cache.go
поставляемый
@@ -18,10 +18,6 @@ type Cache interface {
|
||||
// Purge is used to completely clear the cache.
|
||||
Purge() error
|
||||
|
||||
// Set adds the given key and value to the store without an expiry. If the key already exists,
|
||||
// it will overwrite the previous value.
|
||||
Set(key string, value any) error
|
||||
|
||||
// SetWithDefaultExpiry adds the given key and value to the store with the default expiry. If
|
||||
// the key already exists, it will overwrite the previous value
|
||||
SetWithDefaultExpiry(key string, value any) error
|
||||
@@ -31,16 +27,24 @@ type Cache interface {
|
||||
SetWithExpiry(key string, value any, ttl time.Duration) error
|
||||
|
||||
// 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
|
||||
// Returns ErrKeyNotFound if the key is missing from the cache
|
||||
Get(key string, value any) error
|
||||
|
||||
// GetMulti returns values for multiple keys in a single operation.
|
||||
// Returns ErrKeyNotFound if the key is missing from the cache.
|
||||
GetMulti(keys []string, values []any) []error
|
||||
|
||||
// Remove deletes the value for a given key.
|
||||
Remove(key string) error
|
||||
|
||||
// Keys returns a slice of the keys in the cache.
|
||||
Keys() ([]string, error)
|
||||
// RemoveMulti deletes multiple keys in a single operation.
|
||||
RemoveMulti(keys []string) error
|
||||
|
||||
// Scan allows incremental iteration over the entire key-space
|
||||
// in a performant manner. It provides a callback that consumers
|
||||
// can use to process the keys. If the callback returns an error,
|
||||
// the scan stops, returning the same error.
|
||||
Scan(f func([]string) error) error
|
||||
|
||||
// GetInvalidateClusterEvent returns the cluster event configured when this cache was created.
|
||||
GetInvalidateClusterEvent() model.ClusterEvent
|
||||
|
||||
34
server/platform/services/cache/lru.go
поставляемый
34
server/platform/services/cache/lru.go
поставляемый
@@ -57,12 +57,6 @@ func (l *LRU) Purge() error {
|
||||
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 any) 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 previous value
|
||||
func (l *LRU) SetWithDefaultExpiry(key string, value any) error {
|
||||
@@ -101,11 +95,27 @@ func (l *LRU) Remove(key string) error {
|
||||
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()
|
||||
func (l *LRU) RemoveMulti(keys []string) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
// Note, this is a copy of l.Remove. But we want to avoid
|
||||
// fine-grained locking for every single removal. Therefore,
|
||||
// we copy a bit of code for simplicity.
|
||||
for _, key := range keys {
|
||||
if ent, ok := l.items[key]; ok {
|
||||
l.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan passes the whole slice of keys to the callback in LRU mode.
|
||||
// We don't need this callback style for LRU, but since we share
|
||||
// the same interface with Redis, we maintain parity.
|
||||
func (l *LRU) Scan(f func([]string) error) error {
|
||||
l.lock.RLock()
|
||||
keys := make([]string, l.len)
|
||||
i := 0
|
||||
for ent := l.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
@@ -115,7 +125,9 @@ func (l *LRU) Keys() ([]string, error) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
l.lock.RUnlock()
|
||||
|
||||
return f(keys)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
|
||||
30
server/platform/services/cache/lru_striped.go
поставляемый
30
server/platform/services/cache/lru_striped.go
поставляемый
@@ -4,6 +4,7 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
@@ -62,11 +63,6 @@ func (L LRUStriped) Purge() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set does the same as LRU.Set
|
||||
func (L LRUStriped) Set(key string, value any) error {
|
||||
return L.keyBucket(key).Set(key, value)
|
||||
}
|
||||
|
||||
// SetWithDefaultExpiry does the same as LRU.SetWithDefaultExpiry
|
||||
func (L LRUStriped) SetWithDefaultExpiry(key string, value any) error {
|
||||
return L.keyBucket(key).SetWithDefaultExpiry(key, value)
|
||||
@@ -95,16 +91,22 @@ func (L LRUStriped) Remove(key string) error {
|
||||
return L.keyBucket(key).Remove(key)
|
||||
}
|
||||
|
||||
// Keys does the same as LRU.Keys. However, because this is lock-free, keys might be
|
||||
// inserted or removed from a previously scanned LRU cache.
|
||||
// This is not as precise as using a single LRU instance.
|
||||
func (L LRUStriped) Keys() ([]string, error) {
|
||||
var keys []string
|
||||
for _, lru := range L.buckets {
|
||||
k, _ := lru.Keys() // Keys never returns any error
|
||||
keys = append(keys, k...)
|
||||
// RemoveMulti does the same as LRU.RemoveMulti
|
||||
func (L LRUStriped) RemoveMulti(keys []string) error {
|
||||
var err error
|
||||
for _, key := range keys {
|
||||
err = errors.Join(err, L.keyBucket(key).Remove(key))
|
||||
}
|
||||
return keys, nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Scan is basically a copy of Keys in LRU mode.
|
||||
// See comment in LRU.Scan.
|
||||
func (L LRUStriped) Scan(f func([]string) error) error {
|
||||
for _, lru := range L.buckets {
|
||||
lru.Scan(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len does the same as LRU.Len. As for LRUStriped.Keys, this call cannot be precise.
|
||||
|
||||
4
server/platform/services/cache/lru_striped_bench_test.go
поставляемый
4
server/platform/services/cache/lru_striped_bench_test.go
поставляемый
@@ -43,7 +43,7 @@ func BenchmarkLRUStriped(b *testing.B) {
|
||||
bucketKeys[bucketKey] = append(bucketKeys[bucketKey], key)
|
||||
}
|
||||
for i := 0; i < opts.Size; i++ {
|
||||
cache.Set(keys[i], "preflight")
|
||||
cache.SetWithDefaultExpiry(keys[i], "preflight")
|
||||
}
|
||||
|
||||
wgGet := &sync.WaitGroup{}
|
||||
@@ -58,7 +58,7 @@ func BenchmarkLRUStriped(b *testing.B) {
|
||||
case <-stopSet:
|
||||
return
|
||||
default:
|
||||
_ = cache.Set(keys[i], "ignored")
|
||||
_ = cache.SetWithDefaultExpiry(keys[i], "ignored")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
server/platform/services/cache/lru_striped_test.go
поставляемый
16
server/platform/services/cache/lru_striped_test.go
поставляемый
@@ -41,11 +41,15 @@ func TestNewLRUStriped(t *testing.T) {
|
||||
func TestLRUStripedKeyDistribution(t *testing.T) {
|
||||
dataset := makeLRUPredictableTestData(100)
|
||||
|
||||
scache, err := NewLRUStriped(&CacheOptions{StripedBuckets: 4, Size: len(dataset)})
|
||||
scache, err := NewLRUStriped(&CacheOptions{
|
||||
StripedBuckets: 4,
|
||||
Size: len(dataset),
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
cache := scache.(LRUStriped)
|
||||
for _, kv := range dataset {
|
||||
require.NoError(t, cache.Set(kv[0], kv[1]))
|
||||
require.NoError(t, cache.SetWithDefaultExpiry(kv[0], kv[1]))
|
||||
var out string
|
||||
require.NoError(t, cache.Get(kv[0], &out))
|
||||
require.Equal(t, kv[1], out)
|
||||
@@ -87,13 +91,17 @@ func TestLRUStriped_HashKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUStriped_Get(t *testing.T) {
|
||||
cache, err := NewLRUStriped(&CacheOptions{StripedBuckets: 4, Size: 128})
|
||||
cache, err := NewLRUStriped(&CacheOptions{
|
||||
StripedBuckets: 4,
|
||||
Size: 128,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
var out string
|
||||
require.Equal(t, ErrKeyNotFound, cache.Get("key", &out))
|
||||
require.Zero(t, out)
|
||||
|
||||
require.NoError(t, cache.Set("key", "value"))
|
||||
require.NoError(t, cache.SetWithDefaultExpiry("key", "value"))
|
||||
require.NoError(t, cache.Get("key", &out))
|
||||
require.Equal(t, "value", out)
|
||||
}
|
||||
|
||||
82
server/platform/services/cache/lru_test.go
поставляемый
82
server/platform/services/cache/lru_test.go
поставляемый
@@ -23,7 +23,7 @@ func TestLRU(t *testing.T) {
|
||||
})
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
err := l.Set(fmt.Sprintf("%d", i), i)
|
||||
err := l.SetWithDefaultExpiry(fmt.Sprintf("%d", i), i)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -32,25 +32,40 @@ func TestLRU(t *testing.T) {
|
||||
size := lru.len
|
||||
require.Equalf(t, size, 128, "bad len: %v", size)
|
||||
|
||||
keys, err := l.Keys()
|
||||
require.NoError(t, err)
|
||||
for i, k := range keys {
|
||||
var v int
|
||||
err = l.Get(k, &v)
|
||||
require.NoError(t, err, "bad key: %v", k)
|
||||
require.Equalf(t, fmt.Sprintf("%d", v), k, "bad key: %v", k)
|
||||
require.Equalf(t, i+128, v, "bad value: %v", k)
|
||||
}
|
||||
l.Scan(func(keys []string) error {
|
||||
for i, k := range keys {
|
||||
var v int
|
||||
err := l.Get(k, &v)
|
||||
require.NoError(t, err, "bad key: %v", k)
|
||||
require.Equalf(t, fmt.Sprintf("%d", v), k, "bad key: %v", k)
|
||||
require.Equalf(t, i+128, v, "bad value: %v", k)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
var v int
|
||||
err = l.Get(fmt.Sprintf("%d", i), &v)
|
||||
err := l.Get(fmt.Sprintf("%d", i), &v)
|
||||
require.Equal(t, ErrKeyNotFound, err, "should be evicted %v: %v", i, err)
|
||||
}
|
||||
for i := 128; i < 256; i++ {
|
||||
var v int
|
||||
err = l.Get(fmt.Sprintf("%d", i), &v)
|
||||
err := l.Get(fmt.Sprintf("%d", i), &v)
|
||||
require.NoError(t, err, "should not be evicted %v: %v", i, err)
|
||||
}
|
||||
var v1, v2 int
|
||||
var values = []any{&v1, &v2}
|
||||
errs := l.GetMulti([]string{"128", "129"}, values)
|
||||
for _, err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err := l.RemoveMulti([]string{"128", "129"})
|
||||
require.NoError(t, err)
|
||||
errs = l.GetMulti([]string{"128", "129"}, values)
|
||||
for i, err := range errs {
|
||||
require.Equal(t, ErrKeyNotFound, err, "should be deleted %v: %v", i, err)
|
||||
}
|
||||
|
||||
for i := 128; i < 192; i++ {
|
||||
l.Remove(fmt.Sprintf("%d", i))
|
||||
var v int
|
||||
@@ -63,12 +78,13 @@ func TestLRU(t *testing.T) {
|
||||
require.NoError(t, err, "should exist")
|
||||
require.Equalf(t, 192, v, "bad value: %v", v)
|
||||
|
||||
keys, err = l.Keys()
|
||||
require.NoError(t, err)
|
||||
for i, k := range keys {
|
||||
require.Falsef(t, i < 63 && k != fmt.Sprintf("%d", i+193), "out of order key: %v", k)
|
||||
require.Falsef(t, i == 63 && k != "192", "out of order key: %v", k)
|
||||
}
|
||||
l.Scan(func(keys []string) error {
|
||||
for i, k := range keys {
|
||||
require.Falsef(t, i < 63 && k != fmt.Sprintf("%d", i+193), "out of order key: %v", k)
|
||||
require.Falsef(t, i == 63 && k != "192", "out of order key: %v", k)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
l.Purge()
|
||||
size = lru.len
|
||||
@@ -76,7 +92,7 @@ func TestLRU(t *testing.T) {
|
||||
err = l.Get("200", &v)
|
||||
require.Equal(t, err, ErrKeyNotFound, "should contain nothing")
|
||||
|
||||
err = l.Set("201", 301)
|
||||
err = l.SetWithDefaultExpiry("201", 301)
|
||||
require.NoError(t, err)
|
||||
err = l.Get("201", &v)
|
||||
require.NoError(t, err)
|
||||
@@ -116,7 +132,7 @@ func TestLRUMarshalUnMarshal(t *testing.T) {
|
||||
"key1": 1,
|
||||
"key2": "value2",
|
||||
}
|
||||
err := l.Set("test", value1)
|
||||
err := l.SetWithDefaultExpiry("test", value1)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -196,7 +212,7 @@ func TestLRUMarshalUnMarshal(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err = l.Set("post", post.Clone())
|
||||
err = l.SetWithDefaultExpiry("post", post.Clone())
|
||||
require.NoError(t, err)
|
||||
|
||||
var p model.Post
|
||||
@@ -224,7 +240,7 @@ func TestLRUMarshalUnMarshal(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
err = l.Set("session", session)
|
||||
err = l.SetWithDefaultExpiry("session", session)
|
||||
require.NoError(t, err)
|
||||
var s = &model.Session{}
|
||||
err = l.Get("session", s)
|
||||
@@ -267,7 +283,7 @@ func TestLRUMarshalUnMarshal(t *testing.T) {
|
||||
TermsOfServiceCreateAt: 111111,
|
||||
}
|
||||
|
||||
err = l.Set("user", user)
|
||||
err = l.SetWithDefaultExpiry("user", user)
|
||||
require.NoError(t, err)
|
||||
|
||||
var u *model.User
|
||||
@@ -280,7 +296,7 @@ func TestLRUMarshalUnMarshal(t *testing.T) {
|
||||
|
||||
tt := make(map[string]*model.User)
|
||||
tt["1"] = u
|
||||
err = l.Set("mm", model.UserMap(tt))
|
||||
err = l.SetWithDefaultExpiry("mm", model.UserMap(tt))
|
||||
require.NoError(t, err)
|
||||
|
||||
var out map[string]*model.User
|
||||
@@ -300,7 +316,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", value1)
|
||||
err := l2.SetWithDefaultExpiry("test", value1)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val string
|
||||
@@ -350,7 +366,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", value2)
|
||||
err := l2.SetWithDefaultExpiry("test", value2)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val obj
|
||||
@@ -433,7 +449,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", user)
|
||||
err := l2.SetWithDefaultExpiry("test", user)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val model.User
|
||||
@@ -466,7 +482,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", model.UserMap(uMap))
|
||||
err := l2.SetWithDefaultExpiry("test", model.UserMap(uMap))
|
||||
require.NoError(b, err)
|
||||
|
||||
var val map[string]*model.User
|
||||
@@ -545,7 +561,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", post)
|
||||
err := l2.SetWithDefaultExpiry("test", post)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val model.Post
|
||||
@@ -569,7 +585,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", status)
|
||||
err := l2.SetWithDefaultExpiry("test", status)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val *model.Status
|
||||
@@ -605,7 +621,7 @@ func BenchmarkLRU(b *testing.B) {
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.Set("test", &session)
|
||||
err := l2.SetWithDefaultExpiry("test", &session)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val *model.Session
|
||||
@@ -622,14 +638,14 @@ func TestLRURace(t *testing.T) {
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
var wg sync.WaitGroup
|
||||
l2.Set("test", "value1")
|
||||
l2.SetWithDefaultExpiry("test", "value1")
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
value1 := "simplestring"
|
||||
err := l2.Set("test", value1)
|
||||
err := l2.SetWithDefaultExpiry("test", value1)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
|
||||
60
server/platform/services/cache/mocks/Cache.go
поставляемый
60
server/platform/services/cache/mocks/Cache.go
поставляемый
@@ -72,36 +72,6 @@ func (_m *Cache) GetMulti(keys []string, values []interface{}) []error {
|
||||
return r0
|
||||
}
|
||||
|
||||
// Keys provides a mock function with given fields:
|
||||
func (_m *Cache) Keys() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Keys")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *Cache) Name() string {
|
||||
ret := _m.Called()
|
||||
@@ -156,17 +126,35 @@ func (_m *Cache) Remove(key string) error {
|
||||
return r0
|
||||
}
|
||||
|
||||
// Set provides a mock function with given fields: key, value
|
||||
func (_m *Cache) Set(key string, value interface{}) error {
|
||||
ret := _m.Called(key, value)
|
||||
// RemoveMulti provides a mock function with given fields: keys
|
||||
func (_m *Cache) RemoveMulti(keys []string) error {
|
||||
ret := _m.Called(keys)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Set")
|
||||
panic("no return value specified for RemoveMulti")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(key, value)
|
||||
if rf, ok := ret.Get(0).(func([]string) error); ok {
|
||||
r0 = rf(keys)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Scan provides a mock function with given fields: f
|
||||
func (_m *Cache) Scan(f func([]string) error) error {
|
||||
ret := _m.Called(f)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Scan")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(func([]string) error) error); ok {
|
||||
r0 = rf(f)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
32
server/platform/services/cache/provider_test.go
поставляемый
32
server/platform/services/cache/provider_test.go
поставляемый
@@ -18,15 +18,16 @@ func TestNewCache(t *testing.T) {
|
||||
|
||||
size := 1
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
Size: size,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Set("key1", "val1")
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key2", "val2")
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key3", "val3")
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -35,15 +36,16 @@ func TestNewCache(t *testing.T) {
|
||||
|
||||
size := 1
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
Size: size,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Set("key1", "val1")
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key2", "val2")
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key3", "val3")
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -91,14 +93,15 @@ func TestNewCache_Striped(t *testing.T) {
|
||||
Size: size,
|
||||
Striped: true,
|
||||
StripedBuckets: 1,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Set("key1", "val1")
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key2", "val2")
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key3", "val3")
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -110,14 +113,15 @@ func TestNewCache_Striped(t *testing.T) {
|
||||
Size: size,
|
||||
Striped: true,
|
||||
StripedBuckets: 1,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Set("key1", "val1")
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key2", "val2")
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.Set("key3", "val3")
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
|
||||
86
server/platform/services/cache/redis.go
поставляемый
86
server/platform/services/cache/redis.go
поставляемый
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -38,20 +39,7 @@ func NewRedis(opts *CacheOptions, client rueidis.Client) (*Redis, error) {
|
||||
}
|
||||
|
||||
func (r *Redis) Purge() error {
|
||||
// TODO: move to scan
|
||||
keys, err := r.Keys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Del().
|
||||
Key(keys...).
|
||||
Build(),
|
||||
).Error()
|
||||
}
|
||||
|
||||
func (r *Redis) Set(key string, value any) error {
|
||||
return r.SetWithExpiry(key, value, 0)
|
||||
return r.Scan(r.RemoveMulti)
|
||||
}
|
||||
|
||||
// SetWithDefaultExpiry adds the given key and value to the store with the default expiry. If
|
||||
@@ -156,10 +144,9 @@ func (r *Redis) GetMulti(keys []string, values []any) []error {
|
||||
}()
|
||||
|
||||
errs := make([]error, len(keys))
|
||||
newKeys := make([]string, len(keys))
|
||||
for i := range keys {
|
||||
newKeys[i] = r.name + ":" + keys[i]
|
||||
}
|
||||
newKeys := sliceMapper(keys, func(elem string) string {
|
||||
return r.name + ":" + elem
|
||||
})
|
||||
vals, err := r.client.DoCache(context.Background(),
|
||||
r.client.B().Mget().
|
||||
Key(newKeys...).
|
||||
@@ -227,7 +214,7 @@ func (r *Redis) Remove(key string) error {
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Del", elapsed)
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Remove", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -238,22 +225,61 @@ func (r *Redis) Remove(key string) error {
|
||||
).Error()
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache.
|
||||
func (r *Redis) Keys() ([]string, error) {
|
||||
func (r *Redis) RemoveMulti(keys []string) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Keys", elapsed)
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "RemoveMulti", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO: migrate to a function that works on a batch of keys.
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newKeys := sliceMapper(keys, func(elem string) string {
|
||||
return r.name + ":" + elem
|
||||
})
|
||||
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Keys().
|
||||
Pattern(r.name+":*").
|
||||
r.client.B().Del().
|
||||
Key(newKeys...).
|
||||
Build(),
|
||||
).AsStrSlice()
|
||||
).Error()
|
||||
}
|
||||
|
||||
func (r *Redis) Scan(f func([]string) error) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Scan", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
var scan rueidis.ScanEntry
|
||||
var err error
|
||||
for more := true; more; more = scan.Cursor != 0 {
|
||||
scan, err = r.client.Do(context.Background(),
|
||||
r.client.B().Scan().
|
||||
Cursor(scan.Cursor).
|
||||
Match(r.name+":*").
|
||||
Count(100).
|
||||
Build()).AsScanEntry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
removed := sliceMapper(scan.Elements, func(elem string) string {
|
||||
return strings.TrimPrefix(elem, r.name+":")
|
||||
})
|
||||
err = f(removed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
@@ -285,3 +311,11 @@ func (r *Redis) GetInvalidateClusterEvent() model.ClusterEvent {
|
||||
func (r *Redis) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func sliceMapper[S ~[]E, E, R any](slice S, mapper func(E) R) []R {
|
||||
newSlice := make([]R, len(slice))
|
||||
for i, v := range slice {
|
||||
newSlice[i] = mapper(v)
|
||||
}
|
||||
return newSlice
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user