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
```
Этот коммит содержится в:
Agniva De Sarker
2024-08-13 14:18:25 +05:30
коммит произвёл GitHub
родитель e5842e67a8
Коммит a1012d33eb
19 изменённых файлов: 437 добавлений и 212 удалений

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
}