snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
10
server/platform/services/cache/.mockery.yaml
vendored
Normal file
10
server/platform/services/cache/.mockery.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
with-expecter: false
|
||||
packages:
|
||||
github.com/mattermost/mattermost/server/v8/platform/services/cache:
|
||||
config:
|
||||
all: true
|
||||
dir: "platform/services/cache/mocks"
|
||||
filename: "{{.InterfaceName}}.go"
|
||||
mockname: "{{.InterfaceName}}"
|
||||
outpkg: mocks
|
||||
note: "Regenerate this file using `make cache-mocks`."
|
||||
68
server/platform/services/cache/cache.go
vendored
Normal file
68
server/platform/services/cache/cache.go
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// ErrKeyNotFound is the error when the given key is not found
|
||||
var ErrKeyNotFound = errors.New("key not found")
|
||||
|
||||
// Cache is a representation of a cache store that aims to replace cache.Cache
|
||||
type Cache interface {
|
||||
// Purge is used to completely clear the cache.
|
||||
Purge() 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
|
||||
|
||||
// SetWithExpiry adds the given key and value to the cache with the given expiry. If the key
|
||||
// already exists, it will overwrite the previous value
|
||||
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.
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// Name returns the name of the cache
|
||||
Name() string
|
||||
}
|
||||
|
||||
// ExternalCache is a super-set of the Cache interface with
|
||||
// a couple of more methods that allows for more efficient cache updates.
|
||||
// This can be achieved because the cache is external and an update
|
||||
// is visible to all nodes.
|
||||
type ExternalCache interface {
|
||||
Cache
|
||||
// Increment will increment the
|
||||
// number stored at that key by the value.
|
||||
Increment(key string, val int) error
|
||||
// Decrement will decrement the
|
||||
// number stored at that key by the value.
|
||||
Decrement(key string, val int) error
|
||||
}
|
||||
237
server/platform/services/cache/lru.go
vendored
Normal file
237
server/platform/services/cache/lru.go
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// LRU is a thread-safe fixed size LRU cache.
|
||||
type LRU struct {
|
||||
lock sync.RWMutex
|
||||
size int
|
||||
len int
|
||||
currentGeneration int64
|
||||
evictList *list.List
|
||||
items map[string]*list.Element
|
||||
defaultExpiry time.Duration
|
||||
name string
|
||||
invalidateClusterEvent model.ClusterEvent
|
||||
}
|
||||
|
||||
// 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 *CacheOptions) 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 previous value
|
||||
func (l *LRU) SetWithExpiry(key string, value any, 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 any) error {
|
||||
return l.get(key, value)
|
||||
}
|
||||
|
||||
func (l *LRU) GetMulti(keys []string, values []any) []error {
|
||||
errs := make([]error, 0, len(values))
|
||||
for i, key := range keys {
|
||||
errs = append(errs, l.get(key, values[i]))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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() {
|
||||
e := ent.Value.(*entry)
|
||||
if e.generation == l.currentGeneration {
|
||||
keys[i] = e.key
|
||||
i++
|
||||
}
|
||||
}
|
||||
l.lock.RUnlock()
|
||||
|
||||
return f(keys)
|
||||
}
|
||||
|
||||
// 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() model.ClusterEvent {
|
||||
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 any, 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)
|
||||
} 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 any) 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
162
server/platform/services/cache/lru_striped.go
vendored
Normal file
162
server/platform/services/cache/lru_striped.go
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// LRUStriped keeps LRU caches in buckets in order to lower mutex contention.
|
||||
// This is achieved by hashing the input key to map it to a dedicated bucket.
|
||||
// Each bucket (an LRU cache) has its own lock that helps distributing the lock
|
||||
// contention on multiple threads/cores, leading to less wait times.
|
||||
//
|
||||
// LRUStriped implements the Cache interface with the same behavior as LRU.
|
||||
//
|
||||
// Note that, because of it's distributed nature, the fixed size cannot be strictly respected
|
||||
// and you may have a tiny bit more space for keys than you defined through LRUOptions.
|
||||
// Bucket size is computed as follows: (size / nbuckets) + (size % nbuckets)
|
||||
//
|
||||
// Because of this size limit per bucket, and because of the nature of the data, you
|
||||
// may have buckets filled unevenly, and because of this, keys will be evicted from the entire
|
||||
// cache where a simple LRU wouldn't have. Example:
|
||||
//
|
||||
// Two buckets B1 and B2, of max size 2 each, meaning, theoretically, a max size of 4:
|
||||
// - Say you have a set of 3 keys, they could fill an entire LRU cache.
|
||||
// - But if all those keys are assigned to a single bucket B1, the first key will be evicted from B1
|
||||
// - B2 will remain empty, even though there was enough memory allocated
|
||||
//
|
||||
// With 4 buckets and random UUIDs as keys, the amount of false evictions is around 5%.
|
||||
//
|
||||
// By default, the number of buckets equals the number of cpus returned from runtime.NumCPU.
|
||||
//
|
||||
// This struct is lock-free and intended to be used without lock.
|
||||
type LRUStriped struct {
|
||||
buckets []*LRU
|
||||
name string
|
||||
invalidateClusterEvent model.ClusterEvent
|
||||
}
|
||||
|
||||
func (L LRUStriped) hashkeyMapHash(key string) uint64 {
|
||||
return xxhash.Sum64String(key)
|
||||
}
|
||||
|
||||
func (L LRUStriped) keyBucket(key string) *LRU {
|
||||
return L.buckets[L.hashkeyMapHash(key)%uint64(len(L.buckets))]
|
||||
}
|
||||
|
||||
// Purge loops through each LRU cache for purging. Since LRUStriped doesn't use any lock,
|
||||
// each LRU bucket is purged after another one, which means that keys could still
|
||||
// be present after a call to Purge.
|
||||
func (L LRUStriped) Purge() error {
|
||||
for _, lru := range L.buckets {
|
||||
lru.Purge() // errors from purging LRU can be ignored as they always return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWithDefaultExpiry does the same as LRU.SetWithDefaultExpiry
|
||||
func (L LRUStriped) SetWithDefaultExpiry(key string, value any) error {
|
||||
return L.keyBucket(key).SetWithDefaultExpiry(key, value)
|
||||
}
|
||||
|
||||
// SetWithExpiry does the same as LRU.SetWithExpiry
|
||||
func (L LRUStriped) SetWithExpiry(key string, value any, ttl time.Duration) error {
|
||||
return L.keyBucket(key).SetWithExpiry(key, value, ttl)
|
||||
}
|
||||
|
||||
// Get does the same as LRU.Get
|
||||
func (L LRUStriped) Get(key string, value any) error {
|
||||
return L.keyBucket(key).Get(key, value)
|
||||
}
|
||||
|
||||
func (L LRUStriped) GetMulti(keys []string, values []any) []error {
|
||||
errs := make([]error, 0, len(values))
|
||||
for i, key := range keys {
|
||||
errs = append(errs, L.keyBucket(key).Get(key, values[i]))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Remove does the same as LRU.Remove
|
||||
func (L LRUStriped) Remove(key string) error {
|
||||
return L.keyBucket(key).Remove(key)
|
||||
}
|
||||
|
||||
// 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 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.
|
||||
func (L LRUStriped) Len() (int, error) {
|
||||
var size int
|
||||
for _, lru := range L.buckets {
|
||||
s, _ := lru.Len() // Len never returns any error
|
||||
size += s
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// GetInvalidateClusterEvent does the same as LRU.GetInvalidateClusterEvent
|
||||
func (L LRUStriped) GetInvalidateClusterEvent() model.ClusterEvent {
|
||||
return L.invalidateClusterEvent
|
||||
}
|
||||
|
||||
// Name does the same as LRU.Name
|
||||
func (L LRUStriped) Name() string {
|
||||
return L.name
|
||||
}
|
||||
|
||||
// NewLRUStriped creates a striped LRU cache using the special CacheOptions.StripedBuckets value.
|
||||
// See LRUStriped and CacheOptions for more details.
|
||||
//
|
||||
// Not that in order to prevent false eviction, this LRU cache adds 10% (computation is rounded up) of the
|
||||
// requested size to the total cache size.
|
||||
func NewLRUStriped(opts *CacheOptions) (Cache, error) {
|
||||
if opts.StripedBuckets == 0 {
|
||||
return nil, fmt.Errorf("number of buckets is mandatory")
|
||||
}
|
||||
|
||||
if opts.Size < opts.StripedBuckets {
|
||||
return nil, fmt.Errorf("cache size must at least be equal to the number of buckets")
|
||||
}
|
||||
|
||||
// add 10% to the total size, before splitting
|
||||
opts.Size += int(math.Ceil(float64(opts.Size) * 10.0 / 100.0))
|
||||
// now this is the size for each bucket
|
||||
opts.Size = (opts.Size / opts.StripedBuckets) + (opts.Size % opts.StripedBuckets)
|
||||
|
||||
buckets := make([]*LRU, opts.StripedBuckets)
|
||||
stripedBuckets := opts.StripedBuckets
|
||||
for i := range stripedBuckets {
|
||||
buckets[i] = NewLRU(opts).(*LRU)
|
||||
}
|
||||
|
||||
return LRUStriped{
|
||||
buckets: buckets,
|
||||
invalidateClusterEvent: opts.InvalidateClusterEvent,
|
||||
name: opts.Name,
|
||||
}, nil
|
||||
}
|
||||
94
server/platform/services/cache/lru_striped_bench_test.go
vendored
Normal file
94
server/platform/services/cache/lru_striped_bench_test.go
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
m = 500_000
|
||||
)
|
||||
|
||||
func BenchmarkLRUStriped(b *testing.B) {
|
||||
opts := cache.CacheOptions{
|
||||
Name: "",
|
||||
Size: 128,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
StripedBuckets: runtime.NumCPU() - 1,
|
||||
}
|
||||
|
||||
cache, err := cache.NewLRUStriped(&opts)
|
||||
require.NoError(b, err)
|
||||
|
||||
// prepare keys and initial cache values and set routine
|
||||
keys := make([]string, 0, m)
|
||||
// bucketKeys is to demonstrate that splitted locks is working correctly
|
||||
// by assigning one sequence of key for each bucket.
|
||||
bucketKeys := make([][]string, opts.StripedBuckets)
|
||||
for i := range m {
|
||||
key := fmt.Sprintf("%d-key-%d", i, i)
|
||||
keys = append(keys, key)
|
||||
bucketKey := xxhash.Sum64String(key) % uint64(opts.StripedBuckets)
|
||||
bucketKeys[bucketKey] = append(bucketKeys[bucketKey], key)
|
||||
}
|
||||
size := opts.Size
|
||||
for i := range size {
|
||||
cache.SetWithDefaultExpiry(keys[i], "preflight")
|
||||
}
|
||||
|
||||
wgGet := &sync.WaitGroup{}
|
||||
wgSet := &sync.WaitGroup{}
|
||||
// need buffered chan because if the set routine finished before we write into the chan,
|
||||
// we're left without any consumer, making any write to the chan waiting forever.
|
||||
stopSet := make(chan bool, 1)
|
||||
set := func() {
|
||||
defer wgSet.Done()
|
||||
for i := range m {
|
||||
select {
|
||||
case <-stopSet:
|
||||
return
|
||||
default:
|
||||
_ = cache.SetWithDefaultExpiry(keys[i], "ignored")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get := func(bucket int) {
|
||||
defer wgGet.Done()
|
||||
var out string
|
||||
for i := range m {
|
||||
_ = cache.Get(bucketKeys[bucket][i%opts.Size], &out)
|
||||
}
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
b.StopTimer()
|
||||
wgSet.Add(1)
|
||||
go set()
|
||||
stripedBuckets := opts.StripedBuckets
|
||||
for j := range stripedBuckets {
|
||||
wgGet.Add(1)
|
||||
go get(j)
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
wgGet.Wait()
|
||||
|
||||
// Cleanup
|
||||
b.StopTimer()
|
||||
stopSet <- true
|
||||
wgSet.Wait()
|
||||
b.StartTimer()
|
||||
}
|
||||
}
|
||||
136
server/platform/services/cache/lru_striped_test.go
vendored
Normal file
136
server/platform/services/cache/lru_striped_test.go
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"testing"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func makeLRUPredictableTestData(num int) [][2]string {
|
||||
kv := make([][2]string, num)
|
||||
for i := range kv {
|
||||
kv[i] = [2]string{
|
||||
fmt.Sprintf("%d-key-%d", i, i),
|
||||
fmt.Sprintf("%d-val-%d", i, i),
|
||||
}
|
||||
}
|
||||
return kv
|
||||
}
|
||||
|
||||
func TestNewLRUStriped(t *testing.T) {
|
||||
scache, err := NewLRUStriped(&CacheOptions{StripedBuckets: 3, Size: 20})
|
||||
require.NoError(t, err)
|
||||
|
||||
cache := scache.(LRUStriped)
|
||||
|
||||
require.Len(t, cache.buckets, 3)
|
||||
assert.Equal(t, 8, cache.buckets[0].size)
|
||||
assert.Equal(t, 8, cache.buckets[1].size)
|
||||
assert.Equal(t, 8, cache.buckets[2].size)
|
||||
}
|
||||
|
||||
func TestLRUStripedKeyDistribution(t *testing.T) {
|
||||
dataset := makeLRUPredictableTestData(100)
|
||||
|
||||
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.SetWithDefaultExpiry(kv[0], kv[1]))
|
||||
var out string
|
||||
require.NoError(t, cache.Get(kv[0], &out))
|
||||
require.Equal(t, kv[1], out)
|
||||
}
|
||||
|
||||
require.Len(t, cache.buckets, 4)
|
||||
acc := 0
|
||||
for i := range 4 {
|
||||
clen, err := cache.buckets[i].Len()
|
||||
acc += clen
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, clen, len(dataset)/2/4, "at least 50%/nbuckets of all keys in each bucket")
|
||||
}
|
||||
// because of the limited size of each bucket and the nature of our data,
|
||||
// we may have around 10% of our keys evicted in this scenario. removing 1% because we cannot predict
|
||||
// accurately what is happening with random data.
|
||||
assert.GreaterOrEqual(t, acc, len(dataset)-(len(dataset)*1.0/100.0))
|
||||
}
|
||||
|
||||
func TestLRUStriped_Size(t *testing.T) {
|
||||
scache, err := NewLRUStriped(&CacheOptions{StripedBuckets: 2, Size: 128})
|
||||
require.NoError(t, err)
|
||||
cache := scache.(LRUStriped)
|
||||
acc := 0
|
||||
for _, bucket := range cache.buckets {
|
||||
acc += bucket.size
|
||||
}
|
||||
assert.Equal(t, 128+13+1, acc) // +10% +modulo padding
|
||||
}
|
||||
|
||||
func TestLRUStriped_HashKey(t *testing.T) {
|
||||
scache, err := NewLRUStriped(&CacheOptions{StripedBuckets: 2, Size: 128})
|
||||
require.NoError(t, err)
|
||||
cache := scache.(LRUStriped)
|
||||
first := cache.hashkeyMapHash("key")
|
||||
cache.hashkeyMapHash("other_key_to_ensure_that_result_it’s_not_dependent_on_previous_input")
|
||||
second := cache.hashkeyMapHash("key")
|
||||
require.Equal(t, first, second)
|
||||
}
|
||||
|
||||
func TestLRUStriped_Get(t *testing.T) {
|
||||
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.SetWithDefaultExpiry("key", "value"))
|
||||
require.NoError(t, cache.Get("key", &out))
|
||||
require.Equal(t, "value", out)
|
||||
}
|
||||
|
||||
var hashSink uint64
|
||||
|
||||
func BenchmarkSum64(b *testing.B) {
|
||||
cases := []string{
|
||||
"1",
|
||||
"22",
|
||||
"333",
|
||||
model.NewId(),
|
||||
model.NewId() + model.NewId(),
|
||||
}
|
||||
|
||||
for _, case_ := range cases {
|
||||
b.Run(fmt.Sprintf("maphash_string_len_%d", len(case_)), func(b *testing.B) {
|
||||
seed := maphash.MakeSeed()
|
||||
for b.Loop() {
|
||||
var h maphash.Hash
|
||||
h.SetSeed(seed)
|
||||
h.WriteString(case_) // documentation and code says it never fails
|
||||
hashSink = h.Sum64()
|
||||
}
|
||||
})
|
||||
b.Run(fmt.Sprintf("xxhash_string_len_%d", len(case_)), func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
hashSink = xxhash.Sum64String(case_)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
661
server/platform/services/cache/lru_test.go
vendored
Normal file
661
server/platform/services/cache/lru_test.go
vendored
Normal file
@@ -0,0 +1,661 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestLRU(t *testing.T) {
|
||||
l := NewLRU(&CacheOptions{
|
||||
Size: 128,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
|
||||
for i := range 256 {
|
||||
err := l.SetWithDefaultExpiry(fmt.Sprintf("%d", i), i)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
lru, ok := l.(*LRU)
|
||||
require.True(t, ok)
|
||||
size := lru.len
|
||||
require.Equalf(t, size, 128, "bad len: %v", size)
|
||||
|
||||
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 := range 128 {
|
||||
var v int
|
||||
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)
|
||||
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
|
||||
err = l.Get(fmt.Sprintf("%d", i), &v)
|
||||
require.Equal(t, ErrKeyNotFound, err, "should be deleted %v: %v", i, err)
|
||||
}
|
||||
|
||||
var v int
|
||||
err = l.Get("192", &v) // expect 192 to be last key in l.Keys()
|
||||
require.NoError(t, err, "should exist")
|
||||
require.Equalf(t, 192, v, "bad value: %v", v)
|
||||
|
||||
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
|
||||
require.Equalf(t, size, 0, "bad len: %v", size)
|
||||
err = l.Get("200", &v)
|
||||
require.Equal(t, err, ErrKeyNotFound, "should contain nothing")
|
||||
|
||||
err = l.SetWithDefaultExpiry("201", 301)
|
||||
require.NoError(t, err)
|
||||
err = l.Get("201", &v)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 301, v)
|
||||
}
|
||||
|
||||
func TestLRUExpire(t *testing.T) {
|
||||
l := NewLRU(&CacheOptions{
|
||||
Size: 128,
|
||||
DefaultExpiry: 1 * time.Second,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
|
||||
l.SetWithDefaultExpiry("1", 1)
|
||||
l.SetWithExpiry("3", 3, 0*time.Second)
|
||||
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
var r1 int
|
||||
err := l.Get("1", &r1)
|
||||
require.Equal(t, err, ErrKeyNotFound, "should not exist")
|
||||
|
||||
var r2 int
|
||||
err2 := l.Get("3", &r2)
|
||||
require.NoError(t, err2, "should exist")
|
||||
require.Equal(t, 3, r2)
|
||||
}
|
||||
|
||||
func TestLRUMarshalUnMarshal(t *testing.T) {
|
||||
l := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
|
||||
value1 := map[string]any{
|
||||
"key1": 1,
|
||||
"key2": "value2",
|
||||
}
|
||||
err := l.SetWithDefaultExpiry("test", value1)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
var value2 map[string]any
|
||||
err = l.Get("test", &value2)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, value2["key1"])
|
||||
|
||||
v2, ok := value2["key2"].(string)
|
||||
require.True(t, ok, "unable to cast value")
|
||||
assert.Equal(t, "value2", v2)
|
||||
|
||||
post := model.Post{
|
||||
Id: "id",
|
||||
CreateAt: 11111,
|
||||
UpdateAt: 11111,
|
||||
DeleteAt: 11111,
|
||||
EditAt: 111111,
|
||||
IsPinned: true,
|
||||
UserId: "UserId",
|
||||
ChannelId: "ChannelId",
|
||||
RootId: "RootId",
|
||||
OriginalId: "OriginalId",
|
||||
Message: "OriginalId",
|
||||
MessageSource: "MessageSource",
|
||||
Type: "Type",
|
||||
Props: map[string]any{
|
||||
"key": "val",
|
||||
},
|
||||
Hashtags: "Hashtags",
|
||||
Filenames: []string{"item1", "item2"},
|
||||
FileIds: []string{"item1", "item2"},
|
||||
PendingPostId: "PendingPostId",
|
||||
HasReactions: true,
|
||||
ReplyCount: 11111,
|
||||
Metadata: &model.PostMetadata{
|
||||
Embeds: []*model.PostEmbed{
|
||||
{
|
||||
Type: "Type",
|
||||
URL: "URL",
|
||||
Data: "some data",
|
||||
},
|
||||
{
|
||||
Type: "Type 2",
|
||||
URL: "URL 2",
|
||||
Data: "some data 2",
|
||||
},
|
||||
},
|
||||
Emojis: []*model.Emoji{
|
||||
{
|
||||
Id: "id",
|
||||
Name: "name",
|
||||
},
|
||||
},
|
||||
Files: nil,
|
||||
Images: map[string]*model.PostImage{
|
||||
"key": {
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
Format: "format",
|
||||
FrameCount: 1,
|
||||
},
|
||||
"key2": {
|
||||
Width: 999,
|
||||
Height: 888,
|
||||
Format: "format 2",
|
||||
FrameCount: 1000,
|
||||
},
|
||||
},
|
||||
Reactions: []*model.Reaction{
|
||||
{
|
||||
UserId: "user_id",
|
||||
PostId: "post_id",
|
||||
EmojiName: "emoji_name",
|
||||
CreateAt: 111,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = l.SetWithDefaultExpiry("post", post.Clone())
|
||||
require.NoError(t, err)
|
||||
|
||||
var p model.Post
|
||||
err = l.Get("post", &p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, post.Clone(), p.Clone())
|
||||
|
||||
session := &model.Session{
|
||||
Id: "ty7ia14yuty5bmpt8wmz6da1fw",
|
||||
Token: "79c3iq6nzpycmkkawudanqhg5c",
|
||||
CreateAt: 1595445296960,
|
||||
ExpiresAt: 1598296496960,
|
||||
LastActivityAt: 1595445296960,
|
||||
UserId: "rpgh1q5ra38y9xjn9z8fjctezr",
|
||||
Roles: "system_admin system_user",
|
||||
IsOAuth: false,
|
||||
ExpiredNotify: false,
|
||||
Props: map[string]string{
|
||||
"csrf": "33zb7h7rk3rfffztojn5pxbkxe",
|
||||
"isMobile": "false",
|
||||
"isSaml": "false",
|
||||
"is_guest": "false",
|
||||
"os": "",
|
||||
"platform": "Windows",
|
||||
},
|
||||
}
|
||||
|
||||
err = l.SetWithDefaultExpiry("session", session)
|
||||
require.NoError(t, err)
|
||||
var s = &model.Session{}
|
||||
err = l.Get("session", s)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, session, s)
|
||||
|
||||
user := &model.User{
|
||||
Id: "id",
|
||||
CreateAt: 11111,
|
||||
UpdateAt: 11111,
|
||||
DeleteAt: 11111,
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
AuthService: "AuthService",
|
||||
AuthData: nil,
|
||||
Email: "Email",
|
||||
EmailVerified: true,
|
||||
Nickname: "Nickname",
|
||||
FirstName: "FirstName",
|
||||
LastName: "LastName",
|
||||
Position: "Position",
|
||||
Roles: "Roles",
|
||||
AllowMarketing: true,
|
||||
Props: map[string]string{
|
||||
"key0": "value0",
|
||||
},
|
||||
NotifyProps: map[string]string{
|
||||
"key0": "value0",
|
||||
},
|
||||
LastPasswordUpdate: 111111,
|
||||
LastPictureUpdate: 111111,
|
||||
FailedAttempts: 111111,
|
||||
Locale: "Locale",
|
||||
MfaActive: true,
|
||||
MfaSecret: "MfaSecret",
|
||||
LastActivityAt: 111111,
|
||||
IsBot: true,
|
||||
TermsOfServiceId: "TermsOfServiceId",
|
||||
TermsOfServiceCreateAt: 111111,
|
||||
}
|
||||
|
||||
err = l.SetWithDefaultExpiry("user", user)
|
||||
require.NoError(t, err)
|
||||
|
||||
var u model.User
|
||||
err = l.Get("user", &u)
|
||||
require.NoError(t, err)
|
||||
// msgp returns an empty map instead of a nil map.
|
||||
// This does not make an actual difference in terms of functionality.
|
||||
u.Timezone = nil
|
||||
require.Equal(t, user, &u)
|
||||
|
||||
tt := make(model.UserMap)
|
||||
tt["1"] = &u
|
||||
err = l.SetWithDefaultExpiry("mm", tt)
|
||||
require.NoError(t, err)
|
||||
|
||||
var out model.UserMap
|
||||
err = l.Get("mm", &out)
|
||||
require.NoError(t, err)
|
||||
out["1"].Timezone = nil
|
||||
require.Equal(t, tt, out)
|
||||
}
|
||||
|
||||
func BenchmarkLRU(b *testing.B) {
|
||||
value1 := "simplestring"
|
||||
|
||||
b.Run("simple=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", value1)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val string
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
|
||||
type obj struct {
|
||||
Field1 int
|
||||
Field2 string
|
||||
Field3 struct {
|
||||
Field4 int
|
||||
Field5 string
|
||||
}
|
||||
Field6 map[string]string
|
||||
}
|
||||
|
||||
value2 := obj{
|
||||
1,
|
||||
"field2",
|
||||
struct {
|
||||
Field4 int
|
||||
Field5 string
|
||||
}{
|
||||
6,
|
||||
"field5 is a looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string",
|
||||
},
|
||||
map[string]string{
|
||||
"key0": "value0",
|
||||
"key1": "value value1",
|
||||
"key2": "value value value2",
|
||||
"key3": "value value value value3",
|
||||
"key4": "value value value value value4",
|
||||
"key5": "value value value value value value5",
|
||||
"key6": "value value value value value value value6",
|
||||
"key7": "value value value value value value value value7",
|
||||
"key8": "value value value value value value value value value8",
|
||||
"key9": "value value value value value value value value value value9",
|
||||
},
|
||||
}
|
||||
|
||||
b.Run("complex=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", value2)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val obj
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
|
||||
user := &model.User{
|
||||
Id: "id",
|
||||
CreateAt: 11111,
|
||||
UpdateAt: 11111,
|
||||
DeleteAt: 11111,
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
AuthService: "AuthService",
|
||||
AuthData: nil,
|
||||
Email: "Email",
|
||||
EmailVerified: true,
|
||||
Nickname: "Nickname",
|
||||
FirstName: "FirstName",
|
||||
LastName: "LastName",
|
||||
Position: "Position",
|
||||
Roles: "Roles",
|
||||
AllowMarketing: true,
|
||||
Props: map[string]string{
|
||||
"key0": "value0",
|
||||
"key1": "value value1",
|
||||
"key2": "value value value2",
|
||||
"key3": "value value value value3",
|
||||
"key4": "value value value value value4",
|
||||
"key5": "value value value value value value5",
|
||||
"key6": "value value value value value value value6",
|
||||
"key7": "value value value value value value value value7",
|
||||
"key8": "value value value value value value value value value8",
|
||||
"key9": "value value value value value value value value value value9",
|
||||
},
|
||||
NotifyProps: map[string]string{
|
||||
"key0": "value0",
|
||||
"key1": "value value1",
|
||||
"key2": "value value value2",
|
||||
"key3": "value value value value3",
|
||||
"key4": "value value value value value4",
|
||||
"key5": "value value value value value value5",
|
||||
"key6": "value value value value value value value6",
|
||||
"key7": "value value value value value value value value7",
|
||||
"key8": "value value value value value value value value value8",
|
||||
"key9": "value value value value value value value value value value9",
|
||||
},
|
||||
LastPasswordUpdate: 111111,
|
||||
LastPictureUpdate: 111111,
|
||||
FailedAttempts: 111111,
|
||||
Locale: "Locale",
|
||||
Timezone: map[string]string{
|
||||
"key0": "value0",
|
||||
"key1": "value value1",
|
||||
"key2": "value value value2",
|
||||
"key3": "value value value value3",
|
||||
"key4": "value value value value value4",
|
||||
"key5": "value value value value value value5",
|
||||
"key6": "value value value value value value value6",
|
||||
"key7": "value value value value value value value value7",
|
||||
"key8": "value value value value value value value value value8",
|
||||
"key9": "value value value value value value value value value value9",
|
||||
},
|
||||
MfaActive: true,
|
||||
MfaSecret: "MfaSecret",
|
||||
LastActivityAt: 111111,
|
||||
IsBot: true,
|
||||
BotDescription: "field5 is a looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string",
|
||||
BotLastIconUpdate: 111111,
|
||||
TermsOfServiceId: "TermsOfServiceId",
|
||||
TermsOfServiceCreateAt: 111111,
|
||||
}
|
||||
|
||||
b.Run("User=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", user)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val model.User
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
|
||||
uMap := map[string]*model.User{
|
||||
"id1": {
|
||||
Id: "id1",
|
||||
CreateAt: 1111,
|
||||
UpdateAt: 1112,
|
||||
Username: "user1",
|
||||
Password: "pass",
|
||||
},
|
||||
"id2": {
|
||||
Id: "id2",
|
||||
CreateAt: 1113,
|
||||
UpdateAt: 1114,
|
||||
Username: "user2",
|
||||
Password: "pass2",
|
||||
},
|
||||
}
|
||||
|
||||
b.Run("UserMap=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", model.UserMap(uMap))
|
||||
require.NoError(b, err)
|
||||
|
||||
var val map[string]*model.User
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
|
||||
post := &model.Post{
|
||||
Id: "id",
|
||||
CreateAt: 11111,
|
||||
UpdateAt: 11111,
|
||||
DeleteAt: 11111,
|
||||
EditAt: 111111,
|
||||
IsPinned: true,
|
||||
UserId: "UserId",
|
||||
ChannelId: "ChannelId",
|
||||
RootId: "RootId",
|
||||
OriginalId: "OriginalId",
|
||||
Message: "OriginalId",
|
||||
MessageSource: "MessageSource",
|
||||
Type: "Type",
|
||||
Props: map[string]any{
|
||||
"key": "val",
|
||||
},
|
||||
Hashtags: "Hashtags",
|
||||
Filenames: []string{"item1", "item2"},
|
||||
FileIds: []string{"item1", "item2"},
|
||||
PendingPostId: "PendingPostId",
|
||||
HasReactions: true,
|
||||
|
||||
// Transient data populated before sending a post to the client
|
||||
ReplyCount: 11111,
|
||||
Metadata: &model.PostMetadata{
|
||||
Embeds: []*model.PostEmbed{
|
||||
{
|
||||
Type: "Type",
|
||||
URL: "URL",
|
||||
Data: "some data",
|
||||
},
|
||||
{
|
||||
Type: "Type 2",
|
||||
URL: "URL 2",
|
||||
Data: "some data 2",
|
||||
},
|
||||
},
|
||||
Emojis: []*model.Emoji{
|
||||
{
|
||||
Id: "id",
|
||||
Name: "name",
|
||||
},
|
||||
},
|
||||
Files: nil,
|
||||
Images: map[string]*model.PostImage{
|
||||
"key": {
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
Format: "format",
|
||||
FrameCount: 1,
|
||||
},
|
||||
"key2": {
|
||||
Width: 999,
|
||||
Height: 888,
|
||||
Format: "format 2",
|
||||
FrameCount: 1000,
|
||||
},
|
||||
},
|
||||
Reactions: []*model.Reaction{},
|
||||
},
|
||||
}
|
||||
|
||||
b.Run("Post=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", post)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val model.Post
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
|
||||
status := model.Status{
|
||||
UserId: "UserId",
|
||||
Status: "Status",
|
||||
Manual: true,
|
||||
LastActivityAt: 111111,
|
||||
ActiveChannel: "ActiveChannel",
|
||||
}
|
||||
|
||||
b.Run("Status=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", status)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val *model.Status
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
|
||||
session := model.Session{
|
||||
Id: "ty7ia14yuty5bmpt8wmz6da1fw",
|
||||
Token: "79c3iq6nzpycmkkawudanqhg5c",
|
||||
CreateAt: 1595445296960,
|
||||
ExpiresAt: 1598296496960,
|
||||
LastActivityAt: 1595445296960,
|
||||
UserId: "rpgh1q5ra38y9xjn9z8fjctezr",
|
||||
Roles: "system_admin system_user",
|
||||
IsOAuth: false,
|
||||
ExpiredNotify: false,
|
||||
Props: map[string]string{
|
||||
"csrf": "33zb7h7rk3rfffztojn5pxbkxe",
|
||||
"isMobile": "false",
|
||||
"isSaml": "false",
|
||||
"is_guest": "false",
|
||||
"os": "",
|
||||
"platform": "Windows",
|
||||
},
|
||||
}
|
||||
|
||||
b.Run("Session=new", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
err := l2.SetWithDefaultExpiry("test", &session)
|
||||
require.NoError(b, err)
|
||||
|
||||
var val *model.Session
|
||||
err = l2.Get("test", &val)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLRURace(t *testing.T) {
|
||||
l2 := NewLRU(&CacheOptions{
|
||||
Size: 1,
|
||||
DefaultExpiry: 0,
|
||||
InvalidateClusterEvent: "",
|
||||
})
|
||||
var wg sync.WaitGroup
|
||||
l2.SetWithDefaultExpiry("test", "value1")
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
value1 := "simplestring"
|
||||
err := l2.SetWithDefaultExpiry("test", value1)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
var val string
|
||||
err := l2.Get("test", &val)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
213
server/platform/services/cache/mocks/Cache.go
vendored
Normal file
213
server/platform/services/cache/mocks/Cache.go
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make cache-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
time "time"
|
||||
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Cache is an autogenerated mock type for the Cache type
|
||||
type Cache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: key, value
|
||||
func (_m *Cache) Get(key string, value interface{}) error {
|
||||
ret := _m.Called(key, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(key, value)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetInvalidateClusterEvent provides a mock function with no fields
|
||||
func (_m *Cache) GetInvalidateClusterEvent() model.ClusterEvent {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetInvalidateClusterEvent")
|
||||
}
|
||||
|
||||
var r0 model.ClusterEvent
|
||||
if rf, ok := ret.Get(0).(func() model.ClusterEvent); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(model.ClusterEvent)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetMulti provides a mock function with given fields: keys, values
|
||||
func (_m *Cache) GetMulti(keys []string, values []interface{}) []error {
|
||||
ret := _m.Called(keys, values)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetMulti")
|
||||
}
|
||||
|
||||
var r0 []error
|
||||
if rf, ok := ret.Get(0).(func([]string, []interface{}) []error); ok {
|
||||
r0 = rf(keys, values)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]error)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Name provides a mock function with no fields
|
||||
func (_m *Cache) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Purge provides a mock function with no fields
|
||||
func (_m *Cache) Purge() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Purge")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Remove provides a mock function with given fields: key
|
||||
func (_m *Cache) Remove(key string) error {
|
||||
ret := _m.Called(key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// 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 RemoveMulti")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
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)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetWithDefaultExpiry provides a mock function with given fields: key, value
|
||||
func (_m *Cache) SetWithDefaultExpiry(key string, value interface{}) error {
|
||||
ret := _m.Called(key, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetWithDefaultExpiry")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(key, value)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetWithExpiry provides a mock function with given fields: key, value, ttl
|
||||
func (_m *Cache) SetWithExpiry(key string, value interface{}, ttl time.Duration) error {
|
||||
ret := _m.Called(key, value, ttl)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetWithExpiry")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}, time.Duration) error); ok {
|
||||
r0 = rf(key, value, ttl)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewCache creates a new instance of Cache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewCache(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Cache {
|
||||
mock := &Cache{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
249
server/platform/services/cache/mocks/ExternalCache.go
vendored
Normal file
249
server/platform/services/cache/mocks/ExternalCache.go
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make cache-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
time "time"
|
||||
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ExternalCache is an autogenerated mock type for the ExternalCache type
|
||||
type ExternalCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Decrement provides a mock function with given fields: key, val
|
||||
func (_m *ExternalCache) Decrement(key string, val int) error {
|
||||
ret := _m.Called(key, val)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrement")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, int) error); ok {
|
||||
r0 = rf(key, val)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: key, value
|
||||
func (_m *ExternalCache) Get(key string, value interface{}) error {
|
||||
ret := _m.Called(key, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(key, value)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetInvalidateClusterEvent provides a mock function with no fields
|
||||
func (_m *ExternalCache) GetInvalidateClusterEvent() model.ClusterEvent {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetInvalidateClusterEvent")
|
||||
}
|
||||
|
||||
var r0 model.ClusterEvent
|
||||
if rf, ok := ret.Get(0).(func() model.ClusterEvent); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(model.ClusterEvent)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetMulti provides a mock function with given fields: keys, values
|
||||
func (_m *ExternalCache) GetMulti(keys []string, values []interface{}) []error {
|
||||
ret := _m.Called(keys, values)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetMulti")
|
||||
}
|
||||
|
||||
var r0 []error
|
||||
if rf, ok := ret.Get(0).(func([]string, []interface{}) []error); ok {
|
||||
r0 = rf(keys, values)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]error)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Increment provides a mock function with given fields: key, val
|
||||
func (_m *ExternalCache) Increment(key string, val int) error {
|
||||
ret := _m.Called(key, val)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Increment")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, int) error); ok {
|
||||
r0 = rf(key, val)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Name provides a mock function with no fields
|
||||
func (_m *ExternalCache) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Purge provides a mock function with no fields
|
||||
func (_m *ExternalCache) Purge() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Purge")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Remove provides a mock function with given fields: key
|
||||
func (_m *ExternalCache) Remove(key string) error {
|
||||
ret := _m.Called(key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveMulti provides a mock function with given fields: keys
|
||||
func (_m *ExternalCache) RemoveMulti(keys []string) error {
|
||||
ret := _m.Called(keys)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveMulti")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
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 *ExternalCache) 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)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetWithDefaultExpiry provides a mock function with given fields: key, value
|
||||
func (_m *ExternalCache) SetWithDefaultExpiry(key string, value interface{}) error {
|
||||
ret := _m.Called(key, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetWithDefaultExpiry")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(key, value)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetWithExpiry provides a mock function with given fields: key, value, ttl
|
||||
func (_m *ExternalCache) SetWithExpiry(key string, value interface{}, ttl time.Duration) error {
|
||||
ret := _m.Called(key, value, ttl)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetWithExpiry")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}, time.Duration) error); ok {
|
||||
r0 = rf(key, value, ttl)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewExternalCache creates a new instance of ExternalCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewExternalCache(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ExternalCache {
|
||||
mock := &ExternalCache{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
129
server/platform/services/cache/mocks/Provider.go
vendored
Normal file
129
server/platform/services/cache/mocks/Provider.go
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make cache-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
einterfaces "github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
cache "github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Provider is an autogenerated mock type for the Provider type
|
||||
type Provider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Close provides a mock function with no fields
|
||||
func (_m *Provider) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Connect provides a mock function with no fields
|
||||
func (_m *Provider) Connect() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Connect")
|
||||
}
|
||||
|
||||
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 {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewCache provides a mock function with given fields: opts
|
||||
func (_m *Provider) NewCache(opts *cache.CacheOptions) (cache.Cache, error) {
|
||||
ret := _m.Called(opts)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NewCache")
|
||||
}
|
||||
|
||||
var r0 cache.Cache
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*cache.CacheOptions) (cache.Cache, error)); ok {
|
||||
return rf(opts)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*cache.CacheOptions) cache.Cache); ok {
|
||||
r0 = rf(opts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(cache.Cache)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*cache.CacheOptions) error); ok {
|
||||
r1 = rf(opts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetMetrics provides a mock function with given fields: metrics
|
||||
func (_m *Provider) SetMetrics(metrics einterfaces.MetricsInterface) {
|
||||
_m.Called(metrics)
|
||||
}
|
||||
|
||||
// Type provides a mock function with no fields
|
||||
func (_m *Provider) Type() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Type")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewProvider creates a new instance of Provider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewProvider(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Provider {
|
||||
mock := &Provider{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
146
server/platform/services/cache/provider.go
vendored
Normal file
146
server/platform/services/cache/provider.go
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
"github.com/redis/rueidis"
|
||||
)
|
||||
|
||||
// CacheOptions contains options for initializing a cache
|
||||
type CacheOptions struct {
|
||||
Size int
|
||||
DefaultExpiry time.Duration
|
||||
Name string
|
||||
InvalidateClusterEvent model.ClusterEvent
|
||||
Striped bool
|
||||
// StripedBuckets is used only by LRUStriped and shouldn't be greater than the number
|
||||
// of CPUs available on the machine running this cache.
|
||||
StripedBuckets int
|
||||
}
|
||||
|
||||
// Provider is a provider for Cache
|
||||
type Provider interface {
|
||||
// NewCache creates a new cache with given options.
|
||||
NewCache(opts *CacheOptions) (Cache, error)
|
||||
// Connect opens a new connection to the cache using specific provider parameters.
|
||||
// The returned string contains the status of the response from the cache backend.
|
||||
Connect() (string, error)
|
||||
// SetMetrics
|
||||
SetMetrics(metrics einterfaces.MetricsInterface)
|
||||
// Close releases any resources used by the cache provider.
|
||||
Close() error
|
||||
// Type returns what type of cache it generates.
|
||||
Type() string
|
||||
}
|
||||
|
||||
type cacheProvider struct {
|
||||
}
|
||||
|
||||
// NewProvider creates a new CacheProvider
|
||||
func NewProvider() Provider {
|
||||
return &cacheProvider{}
|
||||
}
|
||||
|
||||
// NewCache creates a new cache with given opts
|
||||
func (c *cacheProvider) NewCache(opts *CacheOptions) (Cache, error) {
|
||||
if opts.Striped {
|
||||
return NewLRUStriped(opts)
|
||||
}
|
||||
return NewLRU(opts), nil
|
||||
}
|
||||
|
||||
// Connect opens a new connection to the cache using specific provider parameters.
|
||||
func (c *cacheProvider) Connect() (string, error) {
|
||||
return "OK", nil
|
||||
}
|
||||
|
||||
func (c *cacheProvider) SetMetrics(metrics einterfaces.MetricsInterface) {
|
||||
}
|
||||
|
||||
// Close releases any resources used by the cache provider.
|
||||
func (c *cacheProvider) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cacheProvider) Type() string {
|
||||
return model.CacheTypeLRU
|
||||
}
|
||||
|
||||
type redisProvider struct {
|
||||
client rueidis.Client
|
||||
cachePrefix string
|
||||
metrics einterfaces.MetricsInterface
|
||||
}
|
||||
|
||||
type RedisOptions struct {
|
||||
RedisAddr string
|
||||
RedisPassword string
|
||||
RedisDB int
|
||||
RedisCachePrefix string
|
||||
DisableCache bool
|
||||
}
|
||||
|
||||
// NewProvider creates a new CacheProvider
|
||||
func NewRedisProvider(opts *RedisOptions) (Provider, error) {
|
||||
client, err := rueidis.NewClient(rueidis.ClientOption{
|
||||
InitAddress: []string{opts.RedisAddr},
|
||||
Password: opts.RedisPassword,
|
||||
SelectDB: opts.RedisDB,
|
||||
ForceSingleClient: true,
|
||||
CacheSizeEachConn: 32 * (1 << 20), // 32MiB local cache size
|
||||
DisableCache: opts.DisableCache,
|
||||
// This is used to collect more commands before flushing to Redis.
|
||||
// This increases latency at the cost of lower CPU usage at Redis.
|
||||
// It's a tradeoff we are willing to make because Redis is only
|
||||
// meant to be used at very high scales. The docs suggest 20us,
|
||||
// but going as high as 250us doesn't make any material difference.
|
||||
MaxFlushDelay: 250 * time.Microsecond,
|
||||
DisableRetry: true,
|
||||
// The default is 10s, which is a bit too high
|
||||
ConnWriteTimeout: 5 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &redisProvider{client: client, cachePrefix: opts.RedisCachePrefix}, nil
|
||||
}
|
||||
|
||||
// NewCache creates a new cache with given opts
|
||||
func (r *redisProvider) NewCache(opts *CacheOptions) (Cache, error) {
|
||||
if r.cachePrefix != "" {
|
||||
opts.Name = r.cachePrefix + ":" + opts.Name
|
||||
}
|
||||
rr, err := NewRedis(opts, r.client)
|
||||
rr.metrics = r.metrics
|
||||
return rr, err
|
||||
}
|
||||
|
||||
// Connect opens a new connection to the cache using specific provider parameters.
|
||||
func (r *redisProvider) Connect() (string, error) {
|
||||
res, err := r.client.Do(context.Background(), r.client.B().Ping().Build()).ToString()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to establish connection with redis: %v", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *redisProvider) SetMetrics(metrics einterfaces.MetricsInterface) {
|
||||
r.metrics = metrics
|
||||
}
|
||||
|
||||
func (r *redisProvider) Type() string {
|
||||
return model.CacheTypeRedis
|
||||
}
|
||||
|
||||
// Close releases any resources used by the cache provider.
|
||||
func (r *redisProvider) Close() error {
|
||||
r.client.Close()
|
||||
return nil
|
||||
}
|
||||
173
server/platform/services/cache/provider_test.go
vendored
Normal file
173
server/platform/services/cache/provider_test.go
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestNewCache(t *testing.T) {
|
||||
t.Run("with only size option given", func(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
size := 1
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("with only size option given", func(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
size := 1
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("with all options specified", func(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
size := 1
|
||||
expiry := 1 * time.Second
|
||||
event := model.ClusterEvent("clusterEvent")
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
Name: "name",
|
||||
DefaultExpiry: expiry,
|
||||
InvalidateClusterEvent: event,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, event, c.GetInvalidateClusterEvent())
|
||||
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(expiry + 1*time.Second)
|
||||
|
||||
var v string
|
||||
err = c.Get("key1", &v)
|
||||
require.Equal(t, ErrKeyNotFound, err)
|
||||
err = c.Get("key2", &v)
|
||||
require.Equal(t, ErrKeyNotFound, err)
|
||||
err = c.Get("key3", &v)
|
||||
require.Equal(t, ErrKeyNotFound, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCache_Striped(t *testing.T) {
|
||||
t.Run("with only size option given", func(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
size := 1
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
Striped: true,
|
||||
StripedBuckets: 1,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("with only size option given", func(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
size := 1
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
Striped: true,
|
||||
StripedBuckets: 1,
|
||||
DefaultExpiry: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("with all options specified", func(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
size := 1
|
||||
expiry := 1 * time.Second
|
||||
event := model.ClusterEvent("clusterEvent")
|
||||
c, err := p.NewCache(&CacheOptions{
|
||||
Size: size,
|
||||
Name: "name",
|
||||
DefaultExpiry: expiry,
|
||||
InvalidateClusterEvent: event,
|
||||
Striped: true,
|
||||
StripedBuckets: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, event, c.GetInvalidateClusterEvent())
|
||||
|
||||
err = c.SetWithDefaultExpiry("key1", "val1")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key2", "val2")
|
||||
require.NoError(t, err)
|
||||
err = c.SetWithDefaultExpiry("key3", "val3")
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(expiry + 1*time.Second)
|
||||
|
||||
var v string
|
||||
err = c.Get("key1", &v)
|
||||
require.Equal(t, ErrKeyNotFound, err)
|
||||
err = c.Get("key2", &v)
|
||||
require.Equal(t, ErrKeyNotFound, err)
|
||||
err = c.Get("key3", &v)
|
||||
require.Equal(t, ErrKeyNotFound, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectClose(t *testing.T) {
|
||||
p := NewProvider()
|
||||
|
||||
_, err := p.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
341
server/platform/services/cache/redis.go
vendored
Normal file
341
server/platform/services/cache/redis.go
vendored
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
|
||||
"github.com/redis/rueidis"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
const clientSideTTL = 5 * time.Minute
|
||||
|
||||
type Redis struct {
|
||||
name string
|
||||
client rueidis.Client
|
||||
defaultExpiry time.Duration
|
||||
metrics einterfaces.MetricsInterface
|
||||
}
|
||||
|
||||
func NewRedis(opts *CacheOptions, client rueidis.Client) (*Redis, error) {
|
||||
if opts.Name == "" {
|
||||
return nil, errors.New("no name specified for cache")
|
||||
}
|
||||
return &Redis{
|
||||
name: opts.Name,
|
||||
defaultExpiry: opts.DefaultExpiry,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Redis) Purge() error {
|
||||
return r.Scan(r.RemoveMulti)
|
||||
}
|
||||
|
||||
// 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 (r *Redis) SetWithDefaultExpiry(key string, value any) error {
|
||||
return r.SetWithExpiry(key, value, r.defaultExpiry)
|
||||
}
|
||||
|
||||
// SetWithExpiry adds the given key and value to the cache with the given expiry. If the key
|
||||
// already exists, it will overwrite the previous value
|
||||
func (r *Redis) SetWithExpiry(key string, value any, ttl time.Duration) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Set", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
var valueString string
|
||||
if intVal, ok := value.(int64); ok {
|
||||
valueString = strconv.Itoa(int(intVal))
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
// Slow path for other structs.
|
||||
buf, err = msgpack.Marshal(value)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valueString = rueidis.BinaryString(buf)
|
||||
}
|
||||
|
||||
setValue := r.client.B().Set().
|
||||
Key(r.name + ":" + key).
|
||||
Value(valueString)
|
||||
|
||||
if ttl > 0 {
|
||||
setValue.Ex(ttl)
|
||||
}
|
||||
|
||||
return r.client.Do(context.Background(), setValue.Build()).Error()
|
||||
}
|
||||
|
||||
// Increment increments the value of the key by the value.
|
||||
func (r *Redis) Increment(key string, val int) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Incr", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Incrby().
|
||||
Key(r.name+":"+key).
|
||||
Increment(int64(val)).
|
||||
Build(),
|
||||
).Error()
|
||||
}
|
||||
|
||||
// Decrement decrements the value of the key by the value.
|
||||
func (r *Redis) Decrement(key string, val int) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Decr", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Decrby().
|
||||
Key(r.name+":"+key).
|
||||
Decrement(int64(val)).
|
||||
Build(),
|
||||
).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
|
||||
func (r *Redis) Get(key string, value any) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Get", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
resp := r.client.DoCache(context.Background(),
|
||||
r.client.B().Get().
|
||||
Key(r.name+":"+key).
|
||||
Cache(),
|
||||
clientSideTTL,
|
||||
)
|
||||
|
||||
var intVal int64
|
||||
var bytesVal []byte
|
||||
var err error
|
||||
vPtr, ok := value.(*int64)
|
||||
if ok {
|
||||
intVal, err = resp.AsInt64()
|
||||
} else {
|
||||
bytesVal, err = resp.AsBytes()
|
||||
}
|
||||
if err != nil {
|
||||
if rueidis.IsRedisNil(err) {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
*vPtr = intVal
|
||||
return nil
|
||||
}
|
||||
|
||||
// We use a fast path for hot structs.
|
||||
if msgpVal, ok := value.(msgp.Unmarshaler); ok {
|
||||
_, err := msgpVal.UnmarshalMsg(bytesVal)
|
||||
return err
|
||||
}
|
||||
|
||||
// Slow path for other structs.
|
||||
return msgpack.Unmarshal(bytesVal, value)
|
||||
}
|
||||
|
||||
// GetMulti uses the MGET primitive to fetch multiple keys in a single operation.
|
||||
func (r *Redis) GetMulti(keys []string, values []any) []error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "GetMulti", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
errs := make([]error, len(keys))
|
||||
newKeys := sliceMapper(keys, func(elem string) string {
|
||||
return r.name + ":" + elem
|
||||
})
|
||||
vals, err := r.client.DoCache(context.Background(),
|
||||
r.client.B().Mget().
|
||||
Key(newKeys...).
|
||||
Cache(),
|
||||
clientSideTTL,
|
||||
).ToArray()
|
||||
if err != nil {
|
||||
for i := range errs {
|
||||
errs[i] = err
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
if len(vals) != len(keys) {
|
||||
for i := range errs {
|
||||
errs[i] = fmt.Errorf("length of returned vals %d, does not match length of keys %d", len(vals), len(keys))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
for i, resp := range vals {
|
||||
if resp.IsNil() {
|
||||
errs[i] = ErrKeyNotFound
|
||||
continue
|
||||
}
|
||||
|
||||
var intVal int64
|
||||
var bytesVal []byte
|
||||
var err error
|
||||
vPtr, ok := values[i].(*int64)
|
||||
if ok {
|
||||
intVal, err = resp.AsInt64()
|
||||
} else {
|
||||
bytesVal, err = resp.AsBytes()
|
||||
}
|
||||
if err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
|
||||
if ok {
|
||||
*vPtr = intVal
|
||||
errs[i] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
// We use a fast path for hot structs.
|
||||
if msgpVal, ok := values[i].(msgp.Unmarshaler); ok {
|
||||
_, err := msgpVal.UnmarshalMsg(bytesVal)
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Slow path for other structs.
|
||||
errs[i] = msgpack.Unmarshal(bytesVal, values[i])
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Remove deletes the value for a given key.
|
||||
func (r *Redis) Remove(key string) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Remove", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Del().
|
||||
Key(r.name+":"+key).
|
||||
Build(),
|
||||
).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, "RemoveMulti", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
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().Del().
|
||||
Key(newKeys...).
|
||||
Build(),
|
||||
).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
|
||||
}
|
||||
|
||||
// GetInvalidateClusterEvent returns the cluster event configured when this cache was created.
|
||||
func (r *Redis) GetInvalidateClusterEvent() model.ClusterEvent {
|
||||
return model.ClusterEventNone
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user