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 Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheAccessControlPolicyStore struct {
store.AccessControlPolicyStore
rootStore *LocalCacheStore
}
func (s *LocalCacheAccessControlPolicyStore) handleClusterInvalidateAccessControlPolicyEtag(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
if err := s.rootStore.accessControlPolicyEtagCache.Purge(); err != nil {
s.rootStore.logger.Warn("failed to purge access control policy etag cache", mlog.Err(err))
}
} else if err := s.rootStore.accessControlPolicyEtagCache.Remove(string(msg.Data)); err != nil {
s.rootStore.logger.Warn("failed to remove access control policy etag cache entry", mlog.Err(err))
}
}
// Keyed by channel ID, with the empty string as the system-scoped-only bucket. Every channel's
// epoch folds in the permission policies, so a permission-policy change has to clear the whole
// cache; a single channel's change only invalidates its own key.
func (s LocalCacheAccessControlPolicyStore) GetEtagEpoch(rctx request.CTX, channelID string) (string, error) {
var epoch string
if err := s.rootStore.doStandardReadCache(s.rootStore.accessControlPolicyEtagCache, channelID, &epoch); err == nil {
return epoch, nil
}
epoch, err := s.AccessControlPolicyStore.GetEtagEpoch(rctx, channelID)
if err != nil {
return "", err
}
s.rootStore.doStandardAddToCache(s.rootStore.accessControlPolicyEtagCache, channelID, epoch)
return epoch, nil
}
func (s LocalCacheAccessControlPolicyStore) InvalidateEtagForChannel(channelID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.accessControlPolicyEtagCache, channelID, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.accessControlPolicyEtagCache.Name())
}
}
func (s LocalCacheAccessControlPolicyStore) ClearEtagCache() {
s.rootStore.doClearCacheCluster(s.rootStore.accessControlPolicyEtagCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.accessControlPolicyEtagCache.Name())
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestAccessControlPolicyStoreCache(t *testing.T) {
channelID := "channel-id"
logger := mlog.CreateConsoleTestLogger(t)
rctx := request.TestContext(t)
t.Run("GetEtagEpoch cached on second call", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
epoch, err := cachedStore.AccessControlPolicy().GetEtagEpoch(rctx, channelID)
require.NoError(t, err)
assert.Equal(t, "100-1", epoch)
mockStore.AccessControlPolicy().(*mocks.AccessControlPolicyStore).AssertNumberOfCalls(t, "GetEtagEpoch", 1)
epoch, err = cachedStore.AccessControlPolicy().GetEtagEpoch(rctx, channelID)
require.NoError(t, err)
assert.Equal(t, "100-1", epoch)
mockStore.AccessControlPolicy().(*mocks.AccessControlPolicyStore).AssertNumberOfCalls(t, "GetEtagEpoch", 1)
})
t.Run("InvalidateEtagForChannel forces a re-query", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
_, err = cachedStore.AccessControlPolicy().GetEtagEpoch(rctx, channelID)
require.NoError(t, err)
mockStore.AccessControlPolicy().(*mocks.AccessControlPolicyStore).AssertNumberOfCalls(t, "GetEtagEpoch", 1)
cachedStore.AccessControlPolicy().InvalidateEtagForChannel(channelID)
_, err = cachedStore.AccessControlPolicy().GetEtagEpoch(rctx, channelID)
require.NoError(t, err)
mockStore.AccessControlPolicy().(*mocks.AccessControlPolicyStore).AssertNumberOfCalls(t, "GetEtagEpoch", 2)
})
t.Run("ClearEtagCache forces a re-query", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
_, err = cachedStore.AccessControlPolicy().GetEtagEpoch(rctx, channelID)
require.NoError(t, err)
mockStore.AccessControlPolicy().(*mocks.AccessControlPolicyStore).AssertNumberOfCalls(t, "GetEtagEpoch", 1)
cachedStore.AccessControlPolicy().ClearEtagCache()
_, err = cachedStore.AccessControlPolicy().GetEtagEpoch(rctx, channelID)
require.NoError(t, err)
mockStore.AccessControlPolicy().(*mocks.AccessControlPolicyStore).AssertNumberOfCalls(t, "GetEtagEpoch", 2)
})
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheAttributesStore struct {
store.AttributesStore
rootStore *LocalCacheStore
}
func (s *LocalCacheAttributesStore) handleClusterInvalidateUserPropertyValuesEpoch(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
if err := s.rootStore.userPropertyValuesEpochCache.Purge(); err != nil {
s.rootStore.logger.Warn("failed to purge user property values epoch cache", mlog.Err(err))
}
} else if err := s.rootStore.userPropertyValuesEpochCache.Remove(string(msg.Data)); err != nil {
s.rootStore.logger.Warn("failed to remove user property values epoch cache entry", mlog.Err(err))
}
}
// Keyed by user ID, so the ABAC-aware post-list ETag doesn't hit PropertyValues on every post GET.
// The App layer invalidates the key on property-value writes.
func (s LocalCacheAttributesStore) GetUserPropertyValuesEpoch(rctx request.CTX, userID string) (string, error) {
var epoch string
if err := s.rootStore.doStandardReadCache(s.rootStore.userPropertyValuesEpochCache, userID, &epoch); err == nil {
return epoch, nil
}
epoch, err := s.AttributesStore.GetUserPropertyValuesEpoch(rctx, userID)
if err != nil {
return "", err
}
s.rootStore.doStandardAddToCache(s.rootStore.userPropertyValuesEpochCache, userID, epoch)
return epoch, nil
}
func (s LocalCacheAttributesStore) InvalidateUserPropertyValuesEpoch(userID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.userPropertyValuesEpochCache, userID, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userPropertyValuesEpochCache.Name())
}
}
func (s LocalCacheAttributesStore) ClearUserPropertyValuesEpochCache() {
s.rootStore.doClearCacheCluster(s.rootStore.userPropertyValuesEpochCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userPropertyValuesEpochCache.Name())
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestAttributesStoreCache(t *testing.T) {
userID := "user-id"
logger := mlog.CreateConsoleTestLogger(t)
rctx := request.TestContext(t)
t.Run("GetUserPropertyValuesEpoch cached on second call", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
epoch, err := cachedStore.Attributes().GetUserPropertyValuesEpoch(rctx, userID)
require.NoError(t, err)
assert.Equal(t, "200-1", epoch)
mockStore.Attributes().(*mocks.AttributesStore).AssertNumberOfCalls(t, "GetUserPropertyValuesEpoch", 1)
epoch, err = cachedStore.Attributes().GetUserPropertyValuesEpoch(rctx, userID)
require.NoError(t, err)
assert.Equal(t, "200-1", epoch)
mockStore.Attributes().(*mocks.AttributesStore).AssertNumberOfCalls(t, "GetUserPropertyValuesEpoch", 1)
})
t.Run("InvalidateUserPropertyValuesEpoch forces a re-query", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
_, err = cachedStore.Attributes().GetUserPropertyValuesEpoch(rctx, userID)
require.NoError(t, err)
mockStore.Attributes().(*mocks.AttributesStore).AssertNumberOfCalls(t, "GetUserPropertyValuesEpoch", 1)
cachedStore.Attributes().InvalidateUserPropertyValuesEpoch(userID)
_, err = cachedStore.Attributes().GetUserPropertyValuesEpoch(rctx, userID)
require.NoError(t, err)
mockStore.Attributes().(*mocks.AttributesStore).AssertNumberOfCalls(t, "GetUserPropertyValuesEpoch", 2)
})
t.Run("ClearUserPropertyValuesEpochCache forces a re-query", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
_, err = cachedStore.Attributes().GetUserPropertyValuesEpoch(rctx, userID)
require.NoError(t, err)
mockStore.Attributes().(*mocks.AttributesStore).AssertNumberOfCalls(t, "GetUserPropertyValuesEpoch", 1)
cachedStore.Attributes().ClearUserPropertyValuesEpochCache()
_, err = cachedStore.Attributes().GetUserPropertyValuesEpoch(rctx, userID)
require.NoError(t, err)
mockStore.Attributes().(*mocks.AttributesStore).AssertNumberOfCalls(t, "GetUserPropertyValuesEpoch", 2)
})
}

View File

@@ -0,0 +1,195 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"fmt"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheAutoTranslationStore struct {
store.AutoTranslationStore
rootStore *LocalCacheStore
}
// Cache key generators
func userAutoTranslationKey(userID, channelID string) string {
return fmt.Sprintf("user:%s:%s", userID, channelID)
}
func userLanguageKey(userID, channelID string) string {
return fmt.Sprintf("lang:%s:%s", userID, channelID)
}
func postTranslationEtagKey(channelID string) string {
return fmt.Sprintf("etag:%s", channelID)
}
// Cluster invalidation handler for user auto-translation cache
func (s *LocalCacheAutoTranslationStore) handleClusterInvalidateUserAutoTranslation(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.userAutoTranslationCache.Purge()
} else {
s.rootStore.userAutoTranslationCache.Remove(string(msg.Data))
}
}
// Cluster invalidation handler for post translation etag cache
func (s *LocalCacheAutoTranslationStore) handleClusterInvalidatePostTranslationEtag(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postTranslationEtagCache.Purge()
} else {
s.rootStore.postTranslationEtagCache.Remove(string(msg.Data))
}
}
// ClearCaches purges all auto-translation caches
func (s LocalCacheAutoTranslationStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.userAutoTranslationCache)
s.rootStore.doClearCacheCluster(s.rootStore.postTranslationEtagCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userAutoTranslationCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.postTranslationEtagCache.Name())
}
}
// IsUserEnabled checks if auto-translation is enabled for a user in a channel (with caching)
func (s LocalCacheAutoTranslationStore) IsUserEnabled(userID, channelID string) (bool, error) {
key := userAutoTranslationKey(userID, channelID)
var enabled bool
if err := s.rootStore.doStandardReadCache(s.rootStore.userAutoTranslationCache, key, &enabled); err == nil {
return enabled, nil
}
enabled, err := s.AutoTranslationStore.IsUserEnabled(userID, channelID)
if err != nil {
return false, err
}
s.rootStore.doStandardAddToCache(s.rootStore.userAutoTranslationCache, key, enabled)
return enabled, nil
}
// GetUserLanguage gets the user's language preference for a channel (with caching)
func (s LocalCacheAutoTranslationStore) GetUserLanguage(userID, channelID string) (string, error) {
key := userLanguageKey(userID, channelID)
var language string
if err := s.rootStore.doStandardReadCache(s.rootStore.userAutoTranslationCache, key, &language); err == nil {
return language, nil
}
language, err := s.AutoTranslationStore.GetUserLanguage(userID, channelID)
if err != nil {
return "", err
}
// Only cache non-empty results
if language != "" {
s.rootStore.doStandardAddToCache(s.rootStore.userAutoTranslationCache, key, language)
}
return language, nil
}
// InvalidateUserAutoTranslation invalidates all user auto-translation caches for a specific user+channel
// This should be called when a user is removed from a channel
func (s LocalCacheAutoTranslationStore) InvalidateUserAutoTranslation(userID, channelID string) {
// Invalidate user auto-translation enabled cache
userKey := userAutoTranslationKey(userID, channelID)
s.rootStore.doInvalidateCacheCluster(s.rootStore.userAutoTranslationCache, userKey, nil)
// Invalidate user language cache
langKey := userLanguageKey(userID, channelID)
s.rootStore.doInvalidateCacheCluster(s.rootStore.userAutoTranslationCache, langKey, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userAutoTranslationCache.Name())
}
}
// InvalidateUserLocaleCache invalidates all language caches for a user across all channels
// This should be called when a user changes their locale
func (s LocalCacheAutoTranslationStore) InvalidateUserLocaleCache(userID string) {
// Scan the cache and find all keys that match "lang:{userID}:*"
prefix := fmt.Sprintf("lang:%s:", userID)
var toDelete []string
err := s.rootStore.userAutoTranslationCache.Scan(func(keys []string) error {
for _, key := range keys {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
toDelete = append(toDelete, key)
}
}
return nil
})
if err != nil {
// If scan fails, just purge the entire cache as fallback
s.rootStore.doClearCacheCluster(s.rootStore.userAutoTranslationCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userAutoTranslationCache.Name())
}
return
}
// Invalidate all matching keys
for _, key := range toDelete {
s.rootStore.doInvalidateCacheCluster(s.rootStore.userAutoTranslationCache, key, nil)
}
if s.rootStore.metrics != nil && len(toDelete) > 0 {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userAutoTranslationCache.Name())
}
}
// GetLatestPostUpdateAtForChannel returns the most recent updateAt timestamp for post translations
// in the given channel (across all locales, with caching)
func (s LocalCacheAutoTranslationStore) GetLatestPostUpdateAtForChannel(channelID string) (int64, error) {
key := postTranslationEtagKey(channelID)
var updateAt int64
if err := s.rootStore.doStandardReadCache(s.rootStore.postTranslationEtagCache, key, &updateAt); err == nil {
return updateAt, nil
}
updateAt, err := s.AutoTranslationStore.GetLatestPostUpdateAtForChannel(channelID)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.postTranslationEtagCache, key, updateAt)
return updateAt, nil
}
// InvalidatePostTranslationEtag invalidates the cached post translation etag for a channel
// This should be called after saving a new post translation
func (s LocalCacheAutoTranslationStore) InvalidatePostTranslationEtag(channelID string) {
key := postTranslationEtagKey(channelID)
s.rootStore.doInvalidateCacheCluster(s.rootStore.postTranslationEtagCache, key, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.postTranslationEtagCache.Name())
}
}
// Save wraps the underlying Save and invalidates the post translation etag cache for post translations
func (s LocalCacheAutoTranslationStore) Save(translation *model.Translation) error {
err := s.AutoTranslationStore.Save(translation)
if err != nil {
return err
}
// Invalidate post translation etag cache only for post translations
if translation.ChannelID != "" && translation.ObjectType == model.TranslationObjectTypePost {
s.InvalidatePostTranslationEtag(translation.ChannelID)
}
return nil
}

View File

@@ -0,0 +1,568 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
type LocalCacheChannelStore struct {
store.ChannelStore
rootStore *LocalCacheStore
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelMemberCounts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelMemberCountsCache.Purge()
} else {
s.rootStore.channelMemberCountsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelPinnedPostCount(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelPinnedPostCountsCache.Purge()
} else {
s.rootStore.channelPinnedPostCountsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelGuestCounts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelGuestCountCache.Purge()
} else {
s.rootStore.channelGuestCountCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelById(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelByIdCache.Purge()
s.rootStore.deliveryTracking.purgeChannels()
} else {
s.rootStore.channelByIdCache.Remove(string(msg.Data))
s.rootStore.deliveryTracking.invalidateChannel(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelForUser(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelMembersForUserCache.Purge()
} else {
s.rootStore.channelMembersForUserCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelMembersNotifyProps(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelMembersNotifyPropsCache.Purge()
} else {
s.rootStore.channelMembersNotifyPropsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelByName(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelByNameCache.Purge()
} else {
s.rootStore.channelByNameCache.Remove(string(msg.Data))
}
}
func (s LocalCacheChannelStore) ClearMembersForUserCache() {
s.rootStore.doClearCacheCluster(s.rootStore.channelMembersForUserCache)
}
func (s LocalCacheChannelStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.channelMemberCountsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelPinnedPostCountsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelGuestCountCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelByIdCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelMembersForUserCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelMembersNotifyPropsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelByNameCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelMemberCountsCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelPinnedPostCountsCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelGuestCountCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelByIdCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelMembersForUserCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelMembersNotifyPropsCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelByNameCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidatePinnedPostCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelPinnedPostCountsCache, channelId, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelPinnedPostCountsCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidateMemberCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMemberCountsCache, channelId, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelMemberCountsCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidateGuestCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelGuestCountCache, channelId, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelGuestCountCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidateChannel(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelByIdCache, channelId, nil)
s.rootStore.deliveryTracking.invalidateChannel(channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelByIdCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidateAllChannelMembersForUser(userId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMembersForUserCache, userId, nil)
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMembersForUserCache, userId+"_deleted", nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelMembersForUserCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMembersNotifyPropsCache, channelId, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelMembersNotifyPropsCache.Name())
}
}
func (s LocalCacheChannelStore) InvalidateChannelByName(teamId, name string) {
props := make(map[string]string)
props["name"] = name
if teamId == "" {
props["id"] = "dm"
} else {
props["id"] = teamId
}
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelByNameCache, teamId+name, props)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.channelByNameCache.Name())
}
}
func (s LocalCacheChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetMemberCount(channelId, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelMemberCountsCache, channelId, count)
}
return count, err
}
func (s LocalCacheChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelGuestCountCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetGuestCount(channelId, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelGuestCountCache, channelId, count)
}
return count, err
}
func (s LocalCacheChannelStore) GetMemberCountFromCache(channelId string) int64 {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
return count
}
count, err := s.GetMemberCount(channelId, true)
if err != nil {
return 0
}
return count
}
func (s LocalCacheChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelPinnedPostCountsCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetPinnedPostCount(channelId, allowFromCache)
if err != nil {
return 0, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelPinnedPostCountsCache, channelId, count)
}
return count, nil
}
func (s LocalCacheChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, channelOptions ...model.ChannelOption) (*model.Channel, error) {
newChannel, err := s.ChannelStore.Save(rctx, channel, maxChannelsPerTeam, channelOptions...)
// Space backing channels are excluded from the generic by-id Get/GetMany (SQL) and resolve
// only through GetChannelOfType; caching them here would let the generic cached lookups
// return them and defeat that exclusion.
if err == nil && !newChannel.IsSpace() {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, newChannel.Id, newChannel)
}
return newChannel, err
}
func (s LocalCacheChannelStore) Update(rctx request.CTX, channel *model.Channel) (*model.Channel, error) {
updatedChannel, err := s.ChannelStore.Update(rctx, channel)
if err == nil {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelByIdCache, channel.Id, nil)
}
return updatedChannel, err
}
func (s LocalCacheChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
if allowFromCache {
var cacheItem *model.Channel
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &cacheItem); err == nil {
return cacheItem, nil
}
}
ch, err := s.ChannelStore.Get(id, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, id, ch)
}
return ch, err
}
func (s LocalCacheChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
var foundChannels []*model.Channel
var channelsToQuery []string
if !allowFromCache {
return s.ChannelStore.GetMany(ids, allowFromCache)
}
toPass := allocateCacheTargets[*model.Channel](len(ids))
errs := s.rootStore.doMultiReadCache(s.rootStore.channelByIdCache, ids, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
s.rootStore.logger.Warn("Error in Channelstore.GetMany: ", mlog.Err(err))
}
channelsToQuery = append(channelsToQuery, ids[i])
} else {
gotChannel := *(toPass[i].(**model.Channel))
if gotChannel != nil {
foundChannels = append(foundChannels, gotChannel)
} else {
s.rootStore.logger.Warn("Found nil channel in GetMany. This is not expected")
}
}
}
if len(channelsToQuery) == 0 {
return foundChannels, nil
}
channels, err := s.ChannelStore.GetMany(channelsToQuery, allowFromCache)
if err != nil {
return nil, err
}
for _, ch := range channels {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, ch.Id, ch)
}
return append(foundChannels, channels...), nil
}
func (s LocalCacheChannelStore) GetAllChannelMembersForUser(rctx request.CTX, userId string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
cache_key := userId
if includeDeleted {
cache_key += "_deleted"
}
if allowFromCache {
var ids model.StringMap
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMembersForUserCache, cache_key, &ids); err == nil {
return ids, nil
}
}
ids, err := s.ChannelStore.GetAllChannelMembersForUser(rctx, userId, allowFromCache, includeDeleted)
if err != nil {
return nil, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelMembersForUserCache, cache_key, ids)
}
return ids, nil
}
func (s LocalCacheChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId string, allowFromCache bool) (map[string]model.StringMap, error) {
if allowFromCache {
var cacheItem map[string]model.StringMap
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMembersNotifyPropsCache, channelId, &cacheItem); err == nil {
return cacheItem, nil
}
}
props, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelId, allowFromCache)
if err != nil {
return nil, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelMembersNotifyPropsCache, channelId, props)
}
return props, nil
}
func (s LocalCacheChannelStore) GetByNamesIncludeDeleted(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) {
return s.getByNames(teamId, names, allowFromCache, true)
}
func (s LocalCacheChannelStore) GetByNames(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) {
return s.getByNames(teamId, names, allowFromCache, false)
}
func (s LocalCacheChannelStore) getByNames(teamId string, names []string, allowFromCache, includeArchivedChannels bool) ([]*model.Channel, error) {
var channels []*model.Channel
if allowFromCache {
var misses []string
visited := make(map[string]struct{})
var newKeys []string
for _, name := range names {
if _, ok := visited[name]; ok {
continue
}
visited[name] = struct{}{}
newKeys = append(newKeys, teamId+name)
}
toPass := allocateCacheTargets[*model.Channel](len(newKeys))
errs := s.rootStore.doMultiReadCache(s.rootStore.channelByNameCache, newKeys, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
s.rootStore.logger.Warn("Error in Channelstore.GetByNames: ", mlog.Err(err))
}
misses = append(misses, strings.TrimPrefix(newKeys[i], teamId))
} else {
gotChannel := *(toPass[i].(**model.Channel))
if (gotChannel != nil) && (includeArchivedChannels || gotChannel.DeleteAt == 0) {
channels = append(channels, gotChannel)
} else if gotChannel == nil {
s.rootStore.logger.Warn("Found nil channel in getByNames. This is not expected")
}
}
}
names = misses
}
if len(names) > 0 {
var dbChannels []*model.Channel
var err error
if includeArchivedChannels {
dbChannels, err = s.ChannelStore.GetByNamesIncludeDeleted(teamId, names, allowFromCache)
} else {
dbChannels, err = s.ChannelStore.GetByNames(teamId, names, allowFromCache)
}
if err != nil {
return nil, err
}
for _, channel := range dbChannels {
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelByNameCache, teamId+channel.Name, channel)
}
channels = append(channels, channel)
}
}
return channels, nil
}
func (s LocalCacheChannelStore) GetByNameIncludeDeleted(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, allowFromCache, true)
}
func (s LocalCacheChannelStore) GetByName(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, allowFromCache, false)
}
func (s LocalCacheChannelStore) getByName(teamId string, name string, allowFromCache, includeArchivedChannels bool) (*model.Channel, error) {
var channel *model.Channel
if allowFromCache {
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByNameCache, teamId+name, &channel); err == nil {
if includeArchivedChannels || channel.DeleteAt == 0 {
return channel, nil
}
}
}
var err error
if includeArchivedChannels {
channel, err = s.ChannelStore.GetByNameIncludeDeleted(teamId, name, allowFromCache)
} else {
channel, err = s.ChannelStore.GetByName(teamId, name, allowFromCache)
}
if err != nil {
return nil, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelByNameCache, teamId+name, channel)
}
return channel, nil
}
func (s LocalCacheChannelStore) SaveMember(rctx request.CTX, member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.SaveMember(rctx, member)
if err != nil {
return nil, err
}
// For redis, directly increment member count.
if externalCache, ok := s.rootStore.channelMemberCountsCache.(cache.ExternalCache); ok {
s.rootStore.doIncrementCache(externalCache, member.ChannelId, 1)
} else {
s.InvalidateMemberCount(member.ChannelId)
}
return member, nil
}
func (s LocalCacheChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
members, err := s.ChannelStore.SaveMultipleMembers(members)
if err != nil {
return nil, err
}
for _, member := range members {
// For redis, directly increment member count.
// It should be possible to group the members from the slice
// by channelID and increment it once per channel. But it depends
// on whether all members are part of the same channel or not.
if externalCache, ok := s.rootStore.channelMemberCountsCache.(cache.ExternalCache); ok {
s.rootStore.doIncrementCache(externalCache, member.ChannelId, 1)
} else {
s.InvalidateMemberCount(member.ChannelId)
}
}
return members, nil
}
func (s LocalCacheChannelStore) GetChannelsMemberCount(channelIDs []string) (_ map[string]int64, err error) {
counts := make(map[string]int64)
remainingChannels := make([]string, 0)
toPass := allocateCacheTargets[int64](len(channelIDs))
errs := s.rootStore.doMultiReadCache(s.rootStore.channelMemberCountsCache, channelIDs, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
s.rootStore.logger.Warn("Error in Channelstore.GetChannelsMemberCount: ", mlog.Err(err))
}
remainingChannels = append(remainingChannels, channelIDs[i])
} else {
gotCount := *(toPass[i].(*int64))
if gotCount != 0 {
counts[channelIDs[i]] = gotCount
}
}
}
if len(remainingChannels) > 0 {
remainingChannels, err := s.ChannelStore.GetChannelsMemberCount(remainingChannels)
if err != nil {
return nil, err
}
for id, count := range remainingChannels {
s.rootStore.doStandardAddToCache(s.rootStore.channelMemberCountsCache, id, count)
counts[id] = count
}
}
return counts, nil
}
func (s LocalCacheChannelStore) UpdateMember(rctx request.CTX, member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.UpdateMember(rctx, member)
if err != nil {
return nil, err
}
s.InvalidateMemberCount(member.ChannelId)
return member, nil
}
func (s LocalCacheChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
members, err := s.ChannelStore.UpdateMultipleMembers(members)
if err != nil {
return nil, err
}
for _, member := range members {
s.InvalidateMemberCount(member.ChannelId)
}
return members, nil
}
func (s LocalCacheChannelStore) RemoveMember(rctx request.CTX, channelId, userId string) error {
err := s.ChannelStore.RemoveMember(rctx, channelId, userId)
if err != nil {
return err
}
// For redis, directly decrement member count.
if externalCache, ok := s.rootStore.channelMemberCountsCache.(cache.ExternalCache); ok {
s.rootStore.doDecrementCache(externalCache, channelId, 1)
} else {
s.InvalidateMemberCount(channelId)
}
return nil
}
func (s LocalCacheChannelStore) RemoveMembers(rctx request.CTX, channelId string, userIds []string) error {
err := s.ChannelStore.RemoveMembers(rctx, channelId, userIds)
if err != nil {
return err
}
// For redis, directly decrement member count.
if externalCache, ok := s.rootStore.channelMemberCountsCache.(cache.ExternalCache); ok {
s.rootStore.doDecrementCache(externalCache, channelId, len(userIds))
} else {
s.InvalidateMemberCount(channelId)
}
return nil
}

View File

@@ -0,0 +1,538 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
cmocks "github.com/mattermost/mattermost/server/v8/platform/services/cache/mocks"
)
func TestChannelStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestReactionStore)
}
func TestChannelStoreChannelMemberCountsCache(t *testing.T) {
countResult := int64(10)
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
count, err := cachedStore.Channel().GetMemberCount("id", true)
require.NoError(t, err)
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
count, err = cachedStore.Channel().GetMemberCount("id", true)
require.NoError(t, err)
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
cachedStore.Channel().GetMemberCount("id", false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
})
t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetMemberCount("id", false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
})
t.Run("first call with GetMemberCountFromCache not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
count := cachedStore.Channel().GetMemberCountFromCache("id")
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
count = cachedStore.Channel().GetMemberCountFromCache("id")
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
})
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
cachedStore.Channel().ClearCaches()
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
})
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
cachedStore.Channel().InvalidateMemberCount("id")
cachedStore.Channel().GetMemberCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
})
}
func TestChannelStoreChannelsMemberCountCache(t *testing.T) {
channelsCountResult := map[string]int64{
"channel1": 10,
"channel2": 20,
}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
channelsCount, err := cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
require.NoError(t, err)
assert.Equal(t, channelsCount, channelsCountResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 1)
channelsCount, err = cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
require.NoError(t, err)
assert.Equal(t, channelsCount, channelsCountResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 1)
})
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 1)
cachedStore.Channel().InvalidateMemberCount("channel1")
cachedStore.Channel().InvalidateMemberCount("channel2")
cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 2)
})
}
func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) {
countResult := int64(10)
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
count, err := cachedStore.Channel().GetPinnedPostCount("id", true)
require.NoError(t, err)
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
count, err = cachedStore.Channel().GetPinnedPostCount("id", true)
require.NoError(t, err)
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
cachedStore.Channel().GetPinnedPostCount("id", false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
})
t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetPinnedPostCount("id", false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
})
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
cachedStore.Channel().ClearCaches()
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
})
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
cachedStore.Channel().InvalidatePinnedPostCount("id")
cachedStore.Channel().GetPinnedPostCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
})
}
func TestChannelStoreGuestCountCache(t *testing.T) {
countResult := int64(12)
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
count, err := cachedStore.Channel().GetGuestCount("id", true)
require.NoError(t, err)
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
count, err = cachedStore.Channel().GetGuestCount("id", true)
require.NoError(t, err)
assert.Equal(t, count, countResult)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
cachedStore.Channel().GetGuestCount("id", false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
})
t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetGuestCount("id", false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
})
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
cachedStore.Channel().ClearCaches()
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
})
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
cachedStore.Channel().InvalidateGuestCount("id")
cachedStore.Channel().GetGuestCount("id", true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
})
}
func TestChannelStoreChannel(t *testing.T) {
channelId := "channel1"
fakeChannel := model.Channel{Id: channelId, Name: "channel1-name"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
channel, err := cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
assert.Equal(t, channel, &fakeChannel)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
channel, err = cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
assert.Equal(t, channel, &fakeChannel)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("first call not cached, second force no cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Channel().Get(channelId, false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call force no cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().Get(channelId, false)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Channel().ClearCaches()
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Channel().InvalidateChannel(channelId)
cachedStore.Channel().Get(channelId, true)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
})
}
func TestChannelStoreSavePopulatesCache(t *testing.T) {
channelId := "channel1"
fakeChannel := model.Channel{Id: channelId, Name: "channel1-name"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("save populates cache so subsequent get does not hit store", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.Channel().Save(request.TestContext(t), &fakeChannel, int64(0))
require.NoError(t, err)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Save", 1)
// Get should be served from cache without hitting the underlying store
channel, err := cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
assert.Equal(t, &fakeChannel, channel)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 0)
})
t.Run("save populates cache, invalidate removes it, get hits store", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.Channel().Save(request.TestContext(t), &fakeChannel, int64(0))
require.NoError(t, err)
cachedStore.Channel().InvalidateChannel(channelId)
_, err = cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
})
}
func TestChannelStoreUpdateInvalidatesCache(t *testing.T) {
channelId := "channel1"
fakeChannel := model.Channel{Id: channelId, Name: "channel1-name"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("update after save invalidates cache, get hits store", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// Save populates cache
_, err = cachedStore.Channel().Save(request.TestContext(t), &fakeChannel, int64(0))
require.NoError(t, err)
// Update invalidates cache
_, err = cachedStore.Channel().Update(request.TestContext(t), &fakeChannel)
require.NoError(t, err)
// Get should miss cache and hit the store
_, err = cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("update after get invalidates cache, next get hits store", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First Get populates cache
_, err = cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
// Update invalidates cache
_, err = cachedStore.Channel().Update(request.TestContext(t), &fakeChannel)
require.NoError(t, err)
// Second Get should miss cache and hit the store again
_, err = cachedStore.Channel().Get(channelId, true)
require.NoError(t, err)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
})
}
func TestChannelStoreGetManyCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
fakeChannel := model.Channel{Id: "channel1", Name: "channel1-name"}
fakeChannel2 := model.Channel{Id: "channel2", Name: "channel2-name"}
channels, err := cachedStore.Channel().GetMany([]string{fakeChannel.Id}, true)
require.NoError(t, err)
assert.ElementsMatch(t, model.ChannelList{&fakeChannel}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMany", 1)
channels, err = cachedStore.Channel().GetMany([]string{fakeChannel.Id}, true)
require.NoError(t, err)
assert.ElementsMatch(t, model.ChannelList{&fakeChannel}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMany", 1)
channels, err = cachedStore.Channel().GetMany([]string{fakeChannel.Id, fakeChannel2.Id}, true)
require.NoError(t, err)
assert.ElementsMatch(t, model.ChannelList{&fakeChannel, &fakeChannel2}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMany", 2)
})
t.Run("passing allowCache=false should bypass cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
fakeChannel := model.Channel{Id: "channel1", Name: "channel1-name"}
channels, err := cachedStore.Channel().GetMany([]string{fakeChannel.Id}, false)
require.NoError(t, err)
assert.ElementsMatch(t, model.ChannelList{&fakeChannel}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMany", 1)
})
}
func TestChannelStoreGetByNamesCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
fakeChannel := model.Channel{Id: "channel1", Name: "channel1-name"}
fakeChannel2 := model.Channel{Id: "channel2", Name: "channel2-name"}
channels, err := cachedStore.Channel().GetByNames("team1", []string{fakeChannel.Name}, true)
require.NoError(t, err)
assert.ElementsMatch(t, []*model.Channel{&fakeChannel}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetByNames", 1)
channels, err = cachedStore.Channel().GetByNames("team1", []string{fakeChannel.Name}, true)
require.NoError(t, err)
assert.ElementsMatch(t, []*model.Channel{&fakeChannel}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetByNames", 1)
channels, err = cachedStore.Channel().GetByNames("team1", []string{fakeChannel.Name, fakeChannel2.Name}, true)
require.NoError(t, err)
assert.ElementsMatch(t, []*model.Channel{&fakeChannel, &fakeChannel2}, channels)
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetByNames", 2)
})
}
func TestChannelStoreGetAllChannelMembersForUser(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cmock := cmocks.NewCache(t)
cmock.On("Get", "u1", mock.AnythingOfType("*model.StringMap")).Return(nil)
cachedStore.channel.rootStore.channelMembersForUserCache = cmock
_, err = cachedStore.Channel().GetAllChannelMembersForUser(request.TestContext(t), "u1", true, false)
require.NoError(t, err)
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
const (
CACHE_KEY_REVIEWER_SETTINGS = "reviewer_settings"
)
type LocalCacheContentFlaggingStore struct {
store.ContentFlaggingStore
rootStore *LocalCacheStore
}
func (s *LocalCacheContentFlaggingStore) handleClusterInvalidateContentFlagging(msg *model.ClusterMessage) {
if err := s.rootStore.contentFlaggingCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge content flagging cache", mlog.Err(err))
}
}
func (s LocalCacheContentFlaggingStore) ClearCaches() {
if err := s.rootStore.contentFlaggingCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge content flagging cache", mlog.Err(err))
}
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.contentFlaggingCache.Name())
}
}
func (s LocalCacheContentFlaggingStore) GetReviewerSettings() (*model.ReviewerIDsSettings, error) {
var reviewerSettings *model.ReviewerIDsSettings
err := s.rootStore.doStandardReadCache(s.rootStore.contentFlaggingCache, CACHE_KEY_REVIEWER_SETTINGS, &reviewerSettings)
if err == nil {
return reviewerSettings, nil
}
reviewerSettings, err = s.ContentFlaggingStore.GetReviewerSettings()
if err != nil {
return nil, err
}
if reviewerSettings != nil {
s.rootStore.doStandardAddToCache(s.rootStore.contentFlaggingCache, CACHE_KEY_REVIEWER_SETTINGS, reviewerSettings)
}
return reviewerSettings, nil
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestContentFlaggingStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestContentFlaggingStore)
}
func TestContentFlaggingStoreGetReviewerSettingsCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
setBasicMock := func(mockStore *mocks.Store, cachedStore LocalCacheStore) {
reviewerSettings := &model.ReviewerIDsSettings{
CommonReviewerIds: []string{"user1", "user2", "user3"},
}
mockContentFlaggingStore := mockStore.ContentFlagging().(*mocks.ContentFlaggingStore)
mockContentFlaggingStore.On("GetReviewerSettings").Return(reviewerSettings, nil)
cachedStore.contentFlagging.ContentFlaggingStore = mockContentFlaggingStore
}
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
setBasicMock(mockStore, cachedStore)
settings, err := cachedStore.ContentFlagging().GetReviewerSettings()
require.NoError(t, err)
require.Len(t, settings.CommonReviewerIds, 3)
require.Contains(t, settings.CommonReviewerIds, "user1")
require.Contains(t, settings.CommonReviewerIds, "user2")
require.Contains(t, settings.CommonReviewerIds, "user3")
mockStore.ContentFlagging().(*mocks.ContentFlaggingStore).AssertNumberOfCalls(t, "GetReviewerSettings", 1)
settings, err = cachedStore.ContentFlagging().GetReviewerSettings()
require.NoError(t, err)
require.Len(t, settings.CommonReviewerIds, 3)
require.Contains(t, settings.CommonReviewerIds, "user1")
require.Contains(t, settings.CommonReviewerIds, "user2")
require.Contains(t, settings.CommonReviewerIds, "user3")
mockStore.ContentFlagging().(*mocks.ContentFlaggingStore).AssertNumberOfCalls(t, "GetReviewerSettings", 1)
})
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
setBasicMock(mockStore, cachedStore)
_, err = cachedStore.ContentFlagging().GetReviewerSettings()
require.NoError(t, err)
mockStore.ContentFlagging().(*mocks.ContentFlaggingStore).AssertNumberOfCalls(t, "GetReviewerSettings", 1)
cachedStore.ContentFlagging().ClearCaches()
_, err = cachedStore.ContentFlagging().GetReviewerSettings()
require.NoError(t, err)
mockStore.ContentFlagging().(*mocks.ContentFlaggingStore).AssertNumberOfCalls(t, "GetReviewerSettings", 2)
})
t.Run("cluster invalidation clears cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
setBasicMock(mockStore, cachedStore)
// First call to populate cache
_, err = cachedStore.ContentFlagging().GetReviewerSettings()
require.NoError(t, err)
mockStore.ContentFlagging().(*mocks.ContentFlaggingStore).AssertNumberOfCalls(t, "GetReviewerSettings", 1)
// Simulate cluster invalidation message
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForContentFlagging,
}
cachedStore.contentFlagging.handleClusterInvalidateContentFlagging(msg)
// Next call should hit the store again
_, err = cachedStore.ContentFlagging().GetReviewerSettings()
require.NoError(t, err)
mockStore.ContentFlagging().(*mocks.ContentFlaggingStore).AssertNumberOfCalls(t, "GetReviewerSettings", 2)
})
}

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"errors"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
const cacheKeyTrackedChannelIDs = "tracked_channel_ids"
// LocalCacheDeliveryTrackingStore memoizes the per-channel eligibility answers that post
// delivery audit logging consults once per recorded delivery.
//
// The two channel caches hold plain bools rather than going through cache.Cache, which
// marshals to msgpack on write and unmarshals on every read. On this path that would allocate
// per record. They are populated lazily as channel ids are seen, so there is no startup load
// and a failed read only affects the channel that triggered it.
type LocalCacheDeliveryTrackingStore struct {
store.DeliveryTrackingStore
rootStore *LocalCacheStore
// trackedChannels memoizes explicit allow-list membership, which changes only when an
// admin saves the list.
trackedChannels *lru.Cache[string, bool]
// trackableChannels memoizes whether a channel is neither a DM nor a GM. Channel types are
// mutable (ConvertGroupMessageToChannel rewrites one in place), so entries are dropped
// alongside channelByIdCache — see LocalCacheChannelStore.InvalidateChannel.
trackableChannels *lru.Cache[string, bool]
}
func (s *LocalCacheDeliveryTrackingStore) handleClusterInvalidateDeliveryTracking(msg *model.ClusterMessage) {
s.purge()
}
func (s *LocalCacheDeliveryTrackingStore) ClearCaches() {
s.purge()
s.purgeChannels()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.deliveryTrackingCache.Name())
}
}
func (s *LocalCacheDeliveryTrackingStore) purge() {
if err := s.rootStore.deliveryTrackingCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge delivery tracking cache", mlog.Err(err))
}
s.trackedChannels.Purge()
}
func (s *LocalCacheDeliveryTrackingStore) SaveTrackedChannelIDs(rctx request.CTX, channelIDs []string) error {
if err := s.DeliveryTrackingStore.SaveTrackedChannelIDs(rctx, channelIDs); err != nil {
return err
}
s.purge()
s.rootStore.doClearCacheCluster(s.rootStore.deliveryTrackingCache)
return nil
}
func (s *LocalCacheDeliveryTrackingStore) GetTrackedChannelIDs(rctx request.CTX) ([]string, error) {
var channelIDs []string
if err := s.rootStore.doStandardReadCache(s.rootStore.deliveryTrackingCache, cacheKeyTrackedChannelIDs, &channelIDs); err == nil {
return channelIDs, nil
}
channelIDs, err := s.DeliveryTrackingStore.GetTrackedChannelIDs(rctx)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.deliveryTrackingCache, cacheKeyTrackedChannelIDs, channelIDs)
return channelIDs, nil
}
func (s *LocalCacheDeliveryTrackingStore) IsChannelTracked(rctx request.CTX, channelID string) (bool, error) {
if cached, ok := s.trackedChannels.Get(channelID); ok {
return cached, nil
}
tracked, err := s.DeliveryTrackingStore.IsChannelTracked(rctx, channelID)
if err != nil {
return false, err
}
s.trackedChannels.Add(channelID, tracked)
return tracked, nil
}
func (s *LocalCacheDeliveryTrackingStore) IsChannelTrackable(rctx request.CTX, channelID string) (bool, error) {
if cached, ok := s.trackableChannels.Get(channelID); ok {
return cached, nil
}
channel, err := s.rootStore.Channel().Get(channelID, true)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return false, err
}
// Not cached: a channel missing now — replica lag, or a type this store does not
// return — may resolve on a later read, and caching would exclude it permanently.
return false, nil
}
trackable := !channel.IsGroupOrDirect()
s.trackableChannels.Add(channelID, trackable)
return trackable, nil
}
// invalidateChannel and purgeChannels are called from the channel store, which is built before
// this one, so they tolerate a nil receiver.
func (s *LocalCacheDeliveryTrackingStore) invalidateChannel(channelID string) {
if s == nil {
return
}
s.trackableChannels.Remove(channelID)
}
func (s *LocalCacheDeliveryTrackingStore) purgeChannels() {
if s == nil {
return
}
s.trackableChannels.Purge()
}

View File

@@ -0,0 +1,198 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestDeliveryTrackingStoreGetTrackedChannelIDsCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
rctx := request.TestContext(t)
setBasicMock := func(mockStore *mocks.Store, cachedStore LocalCacheStore) *mocks.DeliveryTrackingStore {
mockDeliveryTrackingStore := mockStore.DeliveryTracking().(*mocks.DeliveryTrackingStore)
mockDeliveryTrackingStore.On("GetTrackedChannelIDs", mock.Anything).Return([]string{"channel1", "channel2"}, nil)
mockDeliveryTrackingStore.On("SaveTrackedChannelIDs", mock.Anything, mock.Anything).Return(nil)
cachedStore.deliveryTracking.DeliveryTrackingStore = mockDeliveryTrackingStore
return mockDeliveryTrackingStore
}
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
mockDeliveryTrackingStore := setBasicMock(mockStore, cachedStore)
channelIDs, err := cachedStore.DeliveryTracking().GetTrackedChannelIDs(rctx)
require.NoError(t, err)
require.Equal(t, []string{"channel1", "channel2"}, channelIDs)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "GetTrackedChannelIDs", 1)
channelIDs, err = cachedStore.DeliveryTracking().GetTrackedChannelIDs(rctx)
require.NoError(t, err)
require.Equal(t, []string{"channel1", "channel2"}, channelIDs)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "GetTrackedChannelIDs", 1)
})
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
mockDeliveryTrackingStore := setBasicMock(mockStore, cachedStore)
_, err = cachedStore.DeliveryTracking().GetTrackedChannelIDs(rctx)
require.NoError(t, err)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "GetTrackedChannelIDs", 1)
cachedStore.DeliveryTracking().ClearCaches()
_, err = cachedStore.DeliveryTracking().GetTrackedChannelIDs(rctx)
require.NoError(t, err)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "GetTrackedChannelIDs", 2)
})
t.Run("saving the list invalidates the cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
mockDeliveryTrackingStore := setBasicMock(mockStore, cachedStore)
_, err = cachedStore.DeliveryTracking().GetTrackedChannelIDs(rctx)
require.NoError(t, err)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "GetTrackedChannelIDs", 1)
require.NoError(t, cachedStore.DeliveryTracking().SaveTrackedChannelIDs(rctx, []string{"channel3"}))
_, err = cachedStore.DeliveryTracking().GetTrackedChannelIDs(rctx)
require.NoError(t, err)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "GetTrackedChannelIDs", 2)
})
}
func TestDeliveryTrackingStoreChannelEligibilityCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
rctx := request.TestContext(t)
openChannel := model.Channel{Id: "channel1", Type: model.ChannelTypeOpen}
dmChannel := model.Channel{Id: "channel2", Type: model.ChannelTypeDirect}
newCachedStore := func(t *testing.T) (LocalCacheStore, *mocks.DeliveryTrackingStore, *mocks.ChannelStore) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
mockDeliveryTrackingStore := mockStore.DeliveryTracking().(*mocks.DeliveryTrackingStore)
mockDeliveryTrackingStore.On("IsChannelTracked", mock.Anything, mock.Anything).Return(true, nil)
mockDeliveryTrackingStore.On("SaveTrackedChannelIDs", mock.Anything, mock.Anything).Return(nil)
cachedStore.deliveryTracking.DeliveryTrackingStore = mockDeliveryTrackingStore
mockChannelStore := mockStore.Channel().(*mocks.ChannelStore)
mockChannelStore.On("Get", openChannel.Id, true).Return(&openChannel, nil)
mockChannelStore.On("Get", dmChannel.Id, true).Return(&dmChannel, nil)
mockChannelStore.On("Get", "missing", true).Return(nil, store.NewErrNotFound("Channel", "missing"))
return cachedStore, mockDeliveryTrackingStore, mockChannelStore
}
t.Run("IsChannelTracked is resolved once per channel", func(t *testing.T) {
cachedStore, mockDeliveryTrackingStore, _ := newCachedStore(t)
for range 3 {
tracked, err := cachedStore.DeliveryTracking().IsChannelTracked(rctx, "channel1")
require.NoError(t, err)
require.True(t, tracked)
}
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "IsChannelTracked", 1)
_, err := cachedStore.DeliveryTracking().IsChannelTracked(rctx, "channel2")
require.NoError(t, err)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "IsChannelTracked", 2)
})
t.Run("IsChannelTrackable resolves through the channel store once per channel", func(t *testing.T) {
cachedStore, mockDeliveryTrackingStore, mockChannelStore := newCachedStore(t)
for range 3 {
trackable, err := cachedStore.DeliveryTracking().IsChannelTrackable(rctx, openChannel.Id)
require.NoError(t, err)
require.True(t, trackable)
}
mockChannelStore.AssertNumberOfCalls(t, "Get", 1)
// The dedicated store method is never consulted; the channel store is the source.
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "IsChannelTrackable", 0)
})
t.Run("DMs are not trackable", func(t *testing.T) {
cachedStore, _, _ := newCachedStore(t)
trackable, err := cachedStore.DeliveryTracking().IsChannelTrackable(rctx, dmChannel.Id)
require.NoError(t, err)
require.False(t, trackable)
})
t.Run("unknown channels are not trackable and are re-resolved", func(t *testing.T) {
cachedStore, _, mockChannelStore := newCachedStore(t)
for range 2 {
trackable, err := cachedStore.DeliveryTracking().IsChannelTrackable(rctx, "missing")
require.NoError(t, err)
require.False(t, trackable)
}
// Not-found is never memoized: the channel may exist on a later read, and a transient
// miss must not exclude it from auditing for the life of the process.
mockChannelStore.AssertNumberOfCalls(t, "Get", 2)
})
t.Run("saving the list re-resolves tracked but not trackable", func(t *testing.T) {
cachedStore, mockDeliveryTrackingStore, mockChannelStore := newCachedStore(t)
_, err := cachedStore.DeliveryTracking().IsChannelTracked(rctx, openChannel.Id)
require.NoError(t, err)
_, err = cachedStore.DeliveryTracking().IsChannelTrackable(rctx, openChannel.Id)
require.NoError(t, err)
require.NoError(t, cachedStore.DeliveryTracking().SaveTrackedChannelIDs(rctx, []string{"channel3"}))
_, err = cachedStore.DeliveryTracking().IsChannelTracked(rctx, openChannel.Id)
require.NoError(t, err)
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "IsChannelTracked", 2)
// Saving the allow-list purges trackableChannels too, but channelByIdCache still holds
// the channel, so no additional store read is needed to re-resolve it.
_, err = cachedStore.DeliveryTracking().IsChannelTrackable(rctx, openChannel.Id)
require.NoError(t, err)
mockChannelStore.AssertNumberOfCalls(t, "Get", 1)
})
t.Run("errors are not cached", func(t *testing.T) {
mockStore := getMockStore(t)
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, getMockCacheProvider(), logger)
require.NoError(t, err)
mockDeliveryTrackingStore := mockStore.DeliveryTracking().(*mocks.DeliveryTrackingStore)
mockDeliveryTrackingStore.On("IsChannelTracked", mock.Anything, mock.Anything).Return(false, errors.New("boom"))
cachedStore.deliveryTracking.DeliveryTrackingStore = mockDeliveryTrackingStore
for range 2 {
_, err = cachedStore.DeliveryTracking().IsChannelTracked(rctx, "channel1")
require.Error(t, err)
}
mockDeliveryTrackingStore.AssertNumberOfCalls(t, "IsChannelTracked", 2)
})
}

View File

@@ -0,0 +1,222 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"sync"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
)
// emojiNotFoundSentinel is stored in emojiIdCacheByName for names known to be
// absent from the DB; real emoji ids are always 26 characters, so no collision.
const emojiNotFoundSentinel = "-"
type LocalCacheEmojiStore struct {
store.EmojiStore
rootStore *LocalCacheStore
emojiByIdMut sync.Mutex
emojiByIdInvalidations map[string]bool
emojiByNameMut sync.Mutex
emojiByNameInvalidations map[string]bool
}
func (es *LocalCacheEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
savedEmoji, err := es.EmojiStore.Save(emoji)
if err == nil {
// A negative cache entry may exist for this name if it was looked up
// before the emoji was created, so it needs to be invalidated.
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[savedEmoji.Name] = true
es.emojiByNameMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiIdCacheByName, savedEmoji.Name, nil)
}
return savedEmoji, err
}
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiById(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
es.rootStore.emojiCacheById.Purge()
} else {
es.emojiByIdMut.Lock()
es.emojiByIdInvalidations[string(msg.Data)] = true
es.emojiByIdMut.Unlock()
es.rootStore.emojiCacheById.Remove(string(msg.Data))
}
}
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiIdByName(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
es.rootStore.emojiIdCacheByName.Purge()
} else {
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[string(msg.Data)] = true
es.emojiByNameMut.Unlock()
es.rootStore.emojiIdCacheByName.Remove(string(msg.Data))
}
}
func (es *LocalCacheEmojiStore) Get(rctx request.CTX, id string, allowFromCache bool) (*model.Emoji, error) {
if allowFromCache {
if emoji, ok := es.getFromCacheById(id); ok {
return emoji, nil
}
}
// If it was invalidated, then we need to query master.
es.emojiByIdMut.Lock()
if es.emojiByIdInvalidations[id] {
// And then remove the key from the map.
rctx = sqlstore.RequestContextWithMaster(rctx)
delete(es.emojiByIdInvalidations, id)
}
es.emojiByIdMut.Unlock()
emoji, err := es.EmojiStore.Get(rctx, id, allowFromCache)
if allowFromCache && err == nil {
es.addToCache(emoji)
}
return emoji, err
}
func (es *LocalCacheEmojiStore) GetByName(rctx request.CTX, name string, allowFromCache bool) (*model.Emoji, error) {
if id, ok := model.GetSystemEmojiId(name); ok {
return es.Get(rctx, id, allowFromCache)
}
if allowFromCache {
if emoji, ok := es.getFromCacheByName(name); ok {
return emoji, nil
}
}
// If it was invalidated, then we need to query master.
es.emojiByNameMut.Lock()
if es.emojiByNameInvalidations[name] {
rctx = sqlstore.RequestContextWithMaster(rctx)
// And then remove the key from the map.
delete(es.emojiByNameInvalidations, name)
}
es.emojiByNameMut.Unlock()
emoji, err := es.EmojiStore.GetByName(rctx, name, allowFromCache)
if err != nil {
return nil, err
}
if allowFromCache {
es.addToCache(emoji)
}
return emoji, nil
}
func (es *LocalCacheEmojiStore) GetMultipleByName(rctx request.CTX, names []string) ([]*model.Emoji, error) {
emojis := []*model.Emoji{}
remainingEmojiNames := make([]string, 0)
for _, name := range names {
var emojiId string
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiIdCacheByName, name, &emojiId); err == nil {
if emojiId == emojiNotFoundSentinel {
continue
}
if emoji, ok := es.getFromCacheById(emojiId); ok {
emojis = append(emojis, emoji)
continue
}
}
// If it was invalidated, then we need to query master.
es.emojiByNameMut.Lock()
if es.emojiByNameInvalidations[name] {
rctx = sqlstore.RequestContextWithMaster(rctx)
// And then remove the key from the map.
delete(es.emojiByNameInvalidations, name)
}
es.emojiByNameMut.Unlock()
remainingEmojiNames = append(remainingEmojiNames, name)
}
if len(remainingEmojiNames) > 0 {
remainingEmojis, err := es.EmojiStore.GetMultipleByName(rctx, remainingEmojiNames)
if err != nil {
return nil, err
}
foundNames := make(map[string]bool, len(remainingEmojis))
for _, emoji := range remainingEmojis {
es.addToCache(emoji)
emojis = append(emojis, emoji)
foundNames[emoji.Name] = true
}
// Cache the names that don't exist in the database with a short TTL,
// so they don't trigger a DB query on every subsequent lookup. Save
// invalidates these entries when an emoji with such a name is
// created. Names longer than the maximum can never exist and are
// skipped to avoid flooding the cache.
for _, name := range remainingEmojiNames {
if !foundNames[name] && len(name) <= model.EmojiNameMaxLength {
es.rootStore.doStandardAddToCacheWithExpiry(es.rootStore.emojiIdCacheByName, name, emojiNotFoundSentinel, EmojiNotFoundCacheSec*time.Second)
}
}
}
return emojis, nil
}
func (es *LocalCacheEmojiStore) Delete(emoji *model.Emoji, time int64) error {
err := es.EmojiStore.Delete(emoji, time)
if err == nil {
es.removeFromCache(emoji)
}
return err
}
func (es *LocalCacheEmojiStore) addToCache(emoji *model.Emoji) {
es.rootStore.doStandardAddToCache(es.rootStore.emojiCacheById, emoji.Id, emoji)
es.rootStore.doStandardAddToCache(es.rootStore.emojiIdCacheByName, emoji.Name, emoji.Id)
}
func (es *LocalCacheEmojiStore) getFromCacheById(id string) (*model.Emoji, bool) {
var emoji *model.Emoji
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiCacheById, id, &emoji); err == nil {
return emoji, true
}
return nil, false
}
func (es *LocalCacheEmojiStore) getFromCacheByName(name string) (*model.Emoji, bool) {
var emojiId string
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiIdCacheByName, name, &emojiId); err == nil {
if emojiId == emojiNotFoundSentinel {
return nil, false
}
return es.getFromCacheById(emojiId)
}
return nil, false
}
func (es *LocalCacheEmojiStore) removeFromCache(emoji *model.Emoji) {
es.emojiByIdMut.Lock()
es.emojiByIdInvalidations[emoji.Id] = true
es.emojiByIdMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiCacheById, emoji.Id, nil)
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[emoji.Name] = true
es.emojiByNameMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiIdCacheByName, emoji.Name, nil)
}

View File

@@ -0,0 +1,332 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestEmojiStore(t *testing.T) {
StoreTest(t, storetest.TestEmojiStore)
}
func TestEmojiStoreCache(t *testing.T) {
rctx := request.TestContext(t)
logger := mlog.CreateConsoleTestLogger(t)
fakeEmoji := model.Emoji{Id: "123", Name: "name123"}
fakeEmoji2 := model.Emoji{Id: "321", Name: "name321"}
ctxEmoji := model.Emoji{Id: "master", Name: "name123"}
t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emoji, err := cachedStore.Emoji().Get(rctx, "123", true)
require.NoError(t, err)
assert.Equal(t, emoji, &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
emoji, err = cachedStore.Emoji().Get(rctx, "123", true)
require.NoError(t, err)
assert.Equal(t, emoji, &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("GetByName: first call by name not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emoji, err := cachedStore.Emoji().GetByName(rctx, "name123", true)
require.NoError(t, err)
assert.Equal(t, emoji, &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
emoji, err = cachedStore.Emoji().GetByName(rctx, "name123", true)
require.NoError(t, err)
assert.Equal(t, emoji, &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
})
t.Run("GetMultipleByName: first call by name not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123"})
require.NoError(t, err)
require.Len(t, emojis, 1)
assert.Equal(t, emojis[0], &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123"})
require.NoError(t, err)
require.Len(t, emojis, 1)
assert.Equal(t, emojis[0], &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
})
t.Run("GetMultipleByName: multiple elements", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123", "name321"})
require.NoError(t, err)
require.Len(t, emojis, 2)
assert.Equal(t, emojis[0], &fakeEmoji)
assert.Equal(t, emojis[1], &fakeEmoji2)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123"})
require.NoError(t, err)
require.Len(t, emojis, 1)
assert.Equal(t, emojis[0], &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
})
t.Run("GetMultipleByName: missing name cached negatively, second call skips store", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 0)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 0)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
})
t.Run("GetMultipleByName: mix of existing and missing names fully cached after first call", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123", "missing"})
require.NoError(t, err)
require.Len(t, emojis, 1)
assert.Equal(t, emojis[0], &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123", "missing"})
require.NoError(t, err)
require.Len(t, emojis, 1)
assert.Equal(t, emojis[0], &fakeEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
})
t.Run("GetMultipleByName: Save invalidates negative cache entry, emoji becomes visible via master", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 0)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
missingEmoji := model.Emoji{Id: "456", Name: "missing"}
_, err = cachedStore.Emoji().Save(&missingEmoji)
require.NoError(t, err)
// The mock returns the emoji only for a master-routed context, so this
// also pins that Save marks the name for master routing.
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 1)
assert.Equal(t, emojis[0], &missingEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 2)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 1)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 2)
})
t.Run("GetByName: negative cache entry does not short-circuit GetByName and is repaired by it", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 0)
missingEmoji := model.Emoji{Id: "456", Name: "missing"}
emoji, err := cachedStore.Emoji().GetByName(rctx, "missing", true)
require.NoError(t, err)
assert.Equal(t, emoji, &missingEmoji)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{"missing"})
require.NoError(t, err)
require.Len(t, emojis, 1)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
})
t.Run("GetMultipleByName: names longer than the maximum are not cached negatively", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
tooLongName := strings.Repeat("z", model.EmojiNameMaxLength+1)
emojis, err := cachedStore.Emoji().GetMultipleByName(rctx, []string{tooLongName})
require.NoError(t, err)
require.Len(t, emojis, 0)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1)
emojis, err = cachedStore.Emoji().GetMultipleByName(rctx, []string{tooLongName})
require.NoError(t, err)
require.Len(t, emojis, 0)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 2)
})
t.Run("first call by id not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Emoji().Get(rctx, "123", false)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call by name not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Emoji().GetByName(rctx, "name123", false)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("first call by id force not cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().Get(rctx, "123", false)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call by name force not cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().GetByName(rctx, "name123", false)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("first call by id, second call by name and GetMultipleByName cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 0)
cachedStore.Emoji().GetMultipleByName(rctx, []string{"name123"})
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 0)
})
t.Run("first call by name, second call by id cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 0)
})
t.Run("first call by id not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Emoji().Delete(&fakeEmoji, 0)
cachedStore.Emoji().Get(rctx, "123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("call by id, use master", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().Get(rctx, "master", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Emoji().Delete(&ctxEmoji, 0)
cachedStore.Emoji().Get(rctx, "master", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call by name not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Emoji().Delete(&fakeEmoji, 0)
cachedStore.Emoji().GetByName(rctx, "name123", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("call by name, use master", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Emoji().GetByName(rctx, "master", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Emoji().Delete(&ctxEmoji, 0)
cachedStore.Emoji().GetByName(rctx, "master", true)
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
})
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"fmt"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheFileInfoStore struct {
store.FileInfoStore
rootStore *LocalCacheStore
}
func (s *LocalCacheFileInfoStore) handleClusterInvalidateFileInfo(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.fileInfoCache.Purge()
return
}
s.rootStore.fileInfoCache.Remove(string(msg.Data))
}
func (s LocalCacheFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
if !allowFromCache {
return s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
}
cacheKey := postId
if includeDeleted {
cacheKey += "_deleted"
}
var fileInfo []*model.FileInfo
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, cacheKey, &fileInfo); err == nil {
return fileInfo, nil
}
fileInfos, err := s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
if err != nil {
return nil, err
}
if len(fileInfos) > 0 {
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, cacheKey, fileInfos)
}
return fileInfos, nil
}
func (s LocalCacheFileInfoStore) GetByIds(ids []string, includeDeleted, allowFromCache, readFromMaster bool) ([]*model.FileInfo, error) {
if readFromMaster {
return s.FileInfoStore.GetByIds(ids, includeDeleted, false, readFromMaster)
}
if !allowFromCache {
return s.FileInfoStore.GetByIds(ids, includeDeleted, allowFromCache, readFromMaster)
}
var fileIdsToFetch []string
var fileInfos []*model.FileInfo
for _, fileId := range ids {
cacheKey := fmt.Sprintf("%s_%t", fileId, includeDeleted)
var fileInfo *model.FileInfo
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, cacheKey, &fileInfo); err == nil {
fileInfos = append(fileInfos, fileInfo)
} else {
fileIdsToFetch = append(fileIdsToFetch, fileId)
}
}
if len(fileIdsToFetch) > 0 {
fetchedFileInfos, err := s.FileInfoStore.GetByIds(fileIdsToFetch, includeDeleted, false, readFromMaster)
if err != nil {
return nil, err
}
for _, fileInfo := range fetchedFileInfos {
cacheKey := fmt.Sprintf("%s_%t", fileInfo.Id, includeDeleted)
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, cacheKey, fileInfo)
fileInfos = append(fileInfos, fileInfo)
}
}
return fileInfos, nil
}
func (s LocalCacheFileInfoStore) ClearCaches() {
s.rootStore.fileInfoCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.fileInfoCache.Name())
}
}
func (s LocalCacheFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
cacheKey := postId
if deleted {
cacheKey += "_deleted"
}
s.rootStore.doInvalidateCacheCluster(s.rootStore.fileInfoCache, cacheKey, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.fileInfoCache.Name())
}
}
func (s LocalCacheFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
storageUsageKey := "storage_usage"
if includeDeleted {
storageUsageKey += "_deleted"
}
if !allowFromCache {
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
return usage, nil
}
var usage int64
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, storageUsageKey, &usage); err == nil {
return usage, nil
}
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
return usage, nil
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestFileInfoStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestFileInfoStore)
}
func TestFileInfoStoreCache(t *testing.T) {
fakeFileInfo := model.FileInfo{Id: "123", PostId: "123"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
fileInfos, err := cachedStore.FileInfo().GetForPost("123", true, true, true)
require.NoError(t, err)
assert.Equal(t, fileInfos, []*model.FileInfo{&fakeFileInfo})
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
assert.Equal(t, fileInfos, []*model.FileInfo{&fakeFileInfo})
cachedStore.FileInfo().GetForPost("123", true, true, true)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
})
t.Run("first call not cached, second force no cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.FileInfo().GetForPost("123", true, true, true)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
cachedStore.FileInfo().GetForPost("123", true, true, false)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 2)
})
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.FileInfo().GetForPost("123", true, true, true)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
cachedStore.FileInfo().InvalidateFileInfosForPostCache("123", true)
cachedStore.FileInfo().GetForPost("123", true, true, true)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 2)
})
t.Run("GetByIds cache test", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
fileInfos, err := cachedStore.FileInfo().GetByIds([]string{"123"}, true, true, false)
require.NoError(t, err)
assert.Equal(t, []*model.FileInfo{&fakeFileInfo}, fileInfos)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetByIds", 1)
fileInfosCached, err := cachedStore.FileInfo().GetByIds([]string{"123"}, true, true, false)
require.NoError(t, err)
assert.Equal(t, []*model.FileInfo{&fakeFileInfo}, fileInfosCached)
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetByIds", 1)
})
}

View File

@@ -0,0 +1,836 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"runtime"
"time"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/einterfaces"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
const (
ReactionCacheSize = 20000
ReactionCacheSec = 30 * 60
RoleCacheSize = 20000
RoleCacheSec = 30 * 60
SchemeCacheSize = 20000
SchemeCacheSec = 30 * 60
FileInfoCacheSize = 25000
FileInfoCacheSec = 30 * 60
ChannelGuestCountCacheSize = model.ChannelCacheSize
ChannelGuestCountCacheSec = 30 * 60
WebhookCacheSize = 25000
WebhookCacheSec = 15 * 60
EmojiCacheSize = 5000
EmojiCacheSec = 30 * 60
// Kept short so that a lost cluster invalidation or a stale replica read
// cannot hide a newly created emoji for longer than this.
EmojiNotFoundCacheSec = 60
ChannelPinnedPostsCountsCacheSize = model.ChannelCacheSize
ChannelPinnedPostsCountsCacheSec = 30 * 60
AllChannelMembersForUserCacheSize = model.SessionCacheSize
AllChannelMembersForUserCacheDuration = 15 * time.Minute
AllChannelMembersNotifyPropsForChannelCacheSize = model.SessionCacheSize
AllChannelMembersNotifyPropsForChannelCacheDuration = 30 * time.Minute
ChannelCacheDuration = 15 * time.Minute
ChannelMembersCountsCacheSize = model.ChannelCacheSize
ChannelMembersCountsCacheSec = 30 * 60
LastPostsCacheSize = 20000
LastPostsCacheSec = 30 * 60
PostsUsageCacheSize = 1
PostsUsageCacheSec = 30 * 60
TermsOfServiceCacheSize = 20000
TermsOfServiceCacheSec = 30 * 60
LastPostTimeCacheSize = 25000
LastPostTimeCacheSec = 15 * 60
UserProfileByIDCacheSize = 20000
UserProfileByIDSec = 30 * 60
ProfilesInChannelCacheSize = model.ChannelCacheSize
ProfilesInChannelCacheSec = 15 * 60
TeamCacheSize = 20000
TeamCacheSec = 30 * 60
ChannelCacheSec = 15 * 60 // 15 mins
// Auto-translation caches
UserAutoTranslationCacheSize = 50000 // User+channel combos
UserAutoTranslationCacheSec = 15 * 60
PostTranslationEtagCacheSize = 25000 // Channel etags for post translations
PostTranslationEtagCacheSec = 15 * 60
ContentFlaggingCacheSize = 100
DeliveryTrackingCacheSize = 100
DeliveryTrackingChannelCacheSize = 20000
ReadReceiptCacheSize = 50000
TemporaryPostCacheSize = 10000
TemporaryPostCacheMinutes = 60
SessionAttributeCacheSize = model.SessionCacheSize
PropertyFieldCacheSize = 100
PropertyFieldCacheSec = 30 * 60
AccessControlPolicyEtagCacheSize = 25000 // Per-channel render-ETag epochs
AccessControlPolicyEtagCacheSec = 15 * 60
UserPropertyValuesEpochCacheSize = 25000 // Per-user CPA epochs
UserPropertyValuesEpochCacheSec = 15 * 60
)
var clearCacheMessageData = []byte("")
type LocalCacheStore struct {
store.Store
cacheType string
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
logger mlog.LoggerIFace
reaction LocalCacheReactionStore
reactionCache cache.Cache
fileInfo LocalCacheFileInfoStore
fileInfoCache cache.Cache
role LocalCacheRoleStore
roleCache cache.Cache
rolePermissionsCache cache.Cache
scheme LocalCacheSchemeStore
schemeCache cache.Cache
emoji *LocalCacheEmojiStore
emojiCacheById cache.Cache
emojiIdCacheByName cache.Cache
channel LocalCacheChannelStore
channelMemberCountsCache cache.Cache
channelGuestCountCache cache.Cache
channelPinnedPostCountsCache cache.Cache
channelByIdCache cache.Cache
channelMembersForUserCache cache.Cache
channelMembersNotifyPropsCache cache.Cache
channelByNameCache cache.Cache
webhook LocalCacheWebhookStore
webhookCache cache.Cache
post LocalCachePostStore
postLastPostsCache cache.Cache
lastPostTimeCache cache.Cache
postsUsageCache cache.Cache
user *LocalCacheUserStore
allUserCache cache.Cache
userProfileByIdsCache cache.Cache
profilesInChannelCache cache.Cache
team LocalCacheTeamStore
teamAllTeamIdsForUserCache cache.Cache
termsOfService LocalCacheTermsOfServiceStore
termsOfServiceCache cache.Cache
autotranslation LocalCacheAutoTranslationStore
userAutoTranslationCache cache.Cache
postTranslationEtagCache cache.Cache
contentFlagging LocalCacheContentFlaggingStore
contentFlaggingCache cache.Cache
deliveryTracking *LocalCacheDeliveryTrackingStore
deliveryTrackingCache cache.Cache
readReceipt LocalCacheReadReceiptStore
readReceiptCache cache.Cache
readReceiptPostReadersCache cache.Cache
readReceiptPostUnreadCountCache cache.Cache
temporaryPost LocalCacheTemporaryPostStore
temporaryPostCache cache.Cache
sessionAttribute LocalCacheSessionAttributeStore
sessionAttributeCache cache.Cache
propertyField LocalCachePropertyFieldStore
propertyFieldCache cache.Cache
accessControlPolicy LocalCacheAccessControlPolicyStore
accessControlPolicyEtagCache cache.Cache
attributes LocalCacheAttributesStore
userPropertyValuesEpochCache cache.Cache
}
func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterface, cluster einterfaces.ClusterInterface, cacheProvider cache.Provider, logger mlog.LoggerIFace) (localCacheStore LocalCacheStore, err error) {
localCacheStore = LocalCacheStore{
Store: baseStore,
cluster: cluster,
metrics: metrics,
cacheType: cacheProvider.Type(),
logger: logger,
}
// Reactions
if localCacheStore.reactionCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ReactionCacheSize,
Name: "Reaction",
DefaultExpiry: ReactionCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReactions,
}); err != nil {
return
}
localCacheStore.reaction = LocalCacheReactionStore{ReactionStore: baseStore.Reaction(), rootStore: &localCacheStore}
// Roles
if localCacheStore.roleCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: RoleCacheSize,
Name: "Role",
DefaultExpiry: RoleCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRoles,
Striped: true,
StripedBuckets: max(runtime.NumCPU()-1, 1),
}); err != nil {
return
}
if localCacheStore.rolePermissionsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: RoleCacheSize,
Name: "RolePermission",
DefaultExpiry: RoleCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRolePermissions,
}); err != nil {
return
}
localCacheStore.role = LocalCacheRoleStore{RoleStore: baseStore.Role(), rootStore: &localCacheStore}
// Schemes
if localCacheStore.schemeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: SchemeCacheSize,
Name: "Scheme",
DefaultExpiry: SchemeCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForSchemes,
}); err != nil {
return
}
localCacheStore.scheme = LocalCacheSchemeStore{SchemeStore: baseStore.Scheme(), rootStore: &localCacheStore}
// FileInfo
if localCacheStore.fileInfoCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: FileInfoCacheSize,
Name: "FileInfo",
DefaultExpiry: FileInfoCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForFileInfos,
}); err != nil {
return
}
localCacheStore.fileInfo = LocalCacheFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: &localCacheStore}
// Webhooks
if localCacheStore.webhookCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: WebhookCacheSize,
Name: "Webhook",
DefaultExpiry: WebhookCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForWebhooks,
}); err != nil {
return
}
localCacheStore.webhook = LocalCacheWebhookStore{WebhookStore: baseStore.Webhook(), rootStore: &localCacheStore}
// Emojis
if localCacheStore.emojiCacheById, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: EmojiCacheSize,
Name: "EmojiById",
DefaultExpiry: EmojiCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisById,
}); err != nil {
return
}
if localCacheStore.emojiIdCacheByName, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: EmojiCacheSize,
Name: "EmojiByName",
DefaultExpiry: EmojiCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisIdByName,
}); err != nil {
return
}
localCacheStore.emoji = &LocalCacheEmojiStore{
EmojiStore: baseStore.Emoji(),
rootStore: &localCacheStore,
emojiByIdInvalidations: make(map[string]bool),
emojiByNameInvalidations: make(map[string]bool),
}
// Channels
if localCacheStore.channelPinnedPostCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelPinnedPostsCountsCacheSize,
Name: "ChannelPinnedPostsCounts",
DefaultExpiry: ChannelPinnedPostsCountsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts,
}); err != nil {
return
}
if localCacheStore.channelMemberCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelMembersCountsCacheSize,
Name: "ChannelMemberCounts",
DefaultExpiry: ChannelMembersCountsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelMemberCounts,
}); err != nil {
return
}
if localCacheStore.channelGuestCountCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelGuestCountCacheSize,
Name: "ChannelGuestsCount",
DefaultExpiry: ChannelGuestCountCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelGuestCount,
}); err != nil {
return
}
if localCacheStore.channelByIdCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: model.ChannelCacheSize,
Name: "channelById",
DefaultExpiry: ChannelCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannel,
}); err != nil {
return
}
if localCacheStore.channelMembersForUserCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: AllChannelMembersForUserCacheSize,
Name: "ChannelMembersForUser",
DefaultExpiry: AllChannelMembersForUserCacheDuration,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForUser,
}); err != nil {
return
}
if localCacheStore.channelMembersNotifyPropsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: AllChannelMembersNotifyPropsForChannelCacheSize,
Name: "ChannnelMembersNotifyProps",
DefaultExpiry: AllChannelMembersNotifyPropsForChannelCacheDuration,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelMembersNotifyProps,
}); err != nil {
return
}
if localCacheStore.channelByNameCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: model.ChannelCacheSize,
Name: "ChannelByName",
DefaultExpiry: ChannelCacheDuration,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelByName,
}); err != nil {
return
}
localCacheStore.channel = LocalCacheChannelStore{ChannelStore: baseStore.Channel(), rootStore: &localCacheStore}
// Posts
if localCacheStore.postLastPostsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: LastPostsCacheSize,
Name: "LastPost",
DefaultExpiry: LastPostsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPosts,
}); err != nil {
return
}
if localCacheStore.lastPostTimeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: LastPostTimeCacheSize,
Name: "LastPostTime",
DefaultExpiry: LastPostTimeCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPostTime,
}); err != nil {
return
}
if localCacheStore.postsUsageCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: PostsUsageCacheSize,
Name: "PostsUsage",
DefaultExpiry: PostsUsageCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForPostsUsage,
}); err != nil {
return
}
localCacheStore.post = LocalCachePostStore{PostStore: baseStore.Post(), rootStore: &localCacheStore}
// TOS
if localCacheStore.termsOfServiceCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TermsOfServiceCacheSize,
Name: "TermsOfService",
DefaultExpiry: TermsOfServiceCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTermsOfService,
}); err != nil {
return
}
localCacheStore.termsOfService = LocalCacheTermsOfServiceStore{TermsOfServiceStore: baseStore.TermsOfService(), rootStore: &localCacheStore}
// Users
if localCacheStore.allUserCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: 1,
Name: "AllUserProfiles",
DefaultExpiry: UserProfileByIDSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForAllProfiles,
}); err != nil {
return
}
if localCacheStore.userProfileByIdsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: UserProfileByIDCacheSize,
Name: "UserProfileByIds",
DefaultExpiry: UserProfileByIDSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileByIds,
Striped: true,
StripedBuckets: max(runtime.NumCPU()-1, 1),
}); err != nil {
return
}
// Hardcoding this to LRU because of the volume of SCAN calls in case of Redis.
if localCacheStore.profilesInChannelCache, err = cache.NewProvider().NewCache(&cache.CacheOptions{
Size: ProfilesInChannelCacheSize,
Name: "ProfilesInChannel",
DefaultExpiry: ProfilesInChannelCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileInChannel,
}); err != nil {
return
}
localCacheStore.user = &LocalCacheUserStore{
UserStore: baseStore.User(),
rootStore: &localCacheStore,
userProfileByIdsInvalidations: make(map[string]bool),
}
// Teams
if localCacheStore.teamAllTeamIdsForUserCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TeamCacheSize,
Name: "Team",
DefaultExpiry: TeamCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTeams,
}); err != nil {
return
}
localCacheStore.team = LocalCacheTeamStore{TeamStore: baseStore.Team(), rootStore: &localCacheStore}
// Auto-translation
if localCacheStore.userAutoTranslationCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: UserAutoTranslationCacheSize,
Name: "UserAutoTranslation",
DefaultExpiry: UserAutoTranslationCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForUserAutoTranslation,
}); err != nil {
return
}
if localCacheStore.postTranslationEtagCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: PostTranslationEtagCacheSize,
Name: "PostTranslationEtag",
DefaultExpiry: PostTranslationEtagCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForPostTranslationEtag,
}); err != nil {
return
}
localCacheStore.autotranslation = LocalCacheAutoTranslationStore{AutoTranslationStore: baseStore.AutoTranslation(), rootStore: &localCacheStore}
if localCacheStore.contentFlaggingCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ContentFlaggingCacheSize,
Name: "ContentFlagging",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForContentFlagging,
}); err != nil {
return
}
localCacheStore.contentFlagging = LocalCacheContentFlaggingStore{ContentFlaggingStore: baseStore.ContentFlagging(), rootStore: &localCacheStore}
if localCacheStore.deliveryTrackingCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: DeliveryTrackingCacheSize,
Name: "DeliveryTracking",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForDeliveryTracking,
}); err != nil {
return
}
var trackedChannels, trackableChannels *lru.Cache[string, bool]
if trackedChannels, err = lru.New[string, bool](DeliveryTrackingChannelCacheSize); err != nil {
return
}
if trackableChannels, err = lru.New[string, bool](DeliveryTrackingChannelCacheSize); err != nil {
return
}
localCacheStore.deliveryTracking = &LocalCacheDeliveryTrackingStore{
DeliveryTrackingStore: baseStore.DeliveryTracking(),
rootStore: &localCacheStore,
trackedChannels: trackedChannels,
trackableChannels: trackableChannels,
}
// Read Receipts
if localCacheStore.readReceiptCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ReadReceiptCacheSize,
Name: "ReadReceipt",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReadReceipts,
}); err != nil {
return
}
if localCacheStore.readReceiptPostReadersCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ReadReceiptCacheSize,
Name: "ReadReceiptPostReaders",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReadReceipts,
}); err != nil {
return
}
if localCacheStore.readReceiptPostUnreadCountCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ReadReceiptCacheSize,
Name: "ReadReceiptPostUnreadCount",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReadReceipts,
}); err != nil {
return
}
localCacheStore.readReceipt = LocalCacheReadReceiptStore{ReadReceiptStore: baseStore.ReadReceipt(), rootStore: &localCacheStore}
// Temporary Posts
if localCacheStore.temporaryPostCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TemporaryPostCacheSize,
Name: "TemporaryPost",
DefaultExpiry: TemporaryPostCacheMinutes * time.Minute,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTemporaryPosts,
}); err != nil {
return
}
localCacheStore.temporaryPost = LocalCacheTemporaryPostStore{TemporaryPostStore: baseStore.TemporaryPost(), rootStore: &localCacheStore}
// Session Attributes
if localCacheStore.sessionAttributeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: SessionAttributeCacheSize,
Name: "SessionAttribute",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForSessionAttributes,
}); err != nil {
return
}
localCacheStore.sessionAttribute = LocalCacheSessionAttributeStore{SessionAttributeStore: baseStore.SessionAttribute(), rootStore: &localCacheStore}
// Property Fields
if localCacheStore.propertyFieldCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: PropertyFieldCacheSize,
Name: "PropertyField",
DefaultExpiry: PropertyFieldCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForPropertyFields,
}); err != nil {
return
}
localCacheStore.propertyField = LocalCachePropertyFieldStore{PropertyFieldStore: baseStore.PropertyField(), rootStore: &localCacheStore}
// Access Control Policy render-ETag epochs
if localCacheStore.accessControlPolicyEtagCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: AccessControlPolicyEtagCacheSize,
Name: "AccessControlPolicyEtag",
DefaultExpiry: AccessControlPolicyEtagCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForAccessControlPolicyEtag,
}); err != nil {
return
}
localCacheStore.accessControlPolicy = LocalCacheAccessControlPolicyStore{AccessControlPolicyStore: baseStore.AccessControlPolicy(), rootStore: &localCacheStore}
// User property-values (CPA) epochs
if localCacheStore.userPropertyValuesEpochCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: UserPropertyValuesEpochCacheSize,
Name: "UserPropertyValuesEpoch",
DefaultExpiry: UserPropertyValuesEpochCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForUserPropertyValuesEpoch,
}); err != nil {
return
}
localCacheStore.attributes = LocalCacheAttributesStore{AttributesStore: baseStore.Attributes(), rootStore: &localCacheStore}
if cluster != nil {
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReactions, localCacheStore.reaction.handleClusterInvalidateReaction)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRoles, localCacheStore.role.handleClusterInvalidateRole)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRolePermissions, localCacheStore.role.handleClusterInvalidateRolePermissions)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForSchemes, localCacheStore.scheme.handleClusterInvalidateScheme)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForFileInfos, localCacheStore.fileInfo.handleClusterInvalidateFileInfo)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPostTime, localCacheStore.post.handleClusterInvalidateLastPostTime)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForPostsUsage, localCacheStore.post.handleClusterInvalidatePostsUsage)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForWebhooks, localCacheStore.webhook.handleClusterInvalidateWebhook)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisById, localCacheStore.emoji.handleClusterInvalidateEmojiById)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisIdByName, localCacheStore.emoji.handleClusterInvalidateEmojiIdByName)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts, localCacheStore.channel.handleClusterInvalidateChannelPinnedPostCount)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMemberCounts, localCacheStore.channel.handleClusterInvalidateChannelMemberCounts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelGuestCount, localCacheStore.channel.handleClusterInvalidateChannelGuestCounts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannel, localCacheStore.channel.handleClusterInvalidateChannelById)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUser, localCacheStore.channel.handleClusterInvalidateChannelForUser)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMembersNotifyProps, localCacheStore.channel.handleClusterInvalidateChannelMembersNotifyProps)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelByName, localCacheStore.channel.handleClusterInvalidateChannelByName)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPosts, localCacheStore.post.handleClusterInvalidateLastPosts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTermsOfService, localCacheStore.termsOfService.handleClusterInvalidateTermsOfService)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileByIds, localCacheStore.user.handleClusterInvalidateScheme)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileInChannel, localCacheStore.user.handleClusterInvalidateProfilesInChannel)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForAllProfiles, localCacheStore.user.handleClusterInvalidateAllProfiles)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTeams, localCacheStore.team.handleClusterInvalidateTeam)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUserAutoTranslation, localCacheStore.autotranslation.handleClusterInvalidateUserAutoTranslation)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForPostTranslationEtag, localCacheStore.autotranslation.handleClusterInvalidatePostTranslationEtag)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForContentFlagging, localCacheStore.contentFlagging.handleClusterInvalidateContentFlagging)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForDeliveryTracking, localCacheStore.deliveryTracking.handleClusterInvalidateDeliveryTracking)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReadReceipts, localCacheStore.readReceipt.handleClusterInvalidateReadReceipts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTemporaryPosts, localCacheStore.temporaryPost.handleClusterInvalidateTemporaryPosts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForSessionAttributes, localCacheStore.sessionAttribute.handleClusterInvalidateSessionAttributes)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForPropertyFields, localCacheStore.propertyField.handleClusterInvalidatePropertyField)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForAccessControlPolicyEtag, localCacheStore.accessControlPolicy.handleClusterInvalidateAccessControlPolicyEtag)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUserPropertyValuesEpoch, localCacheStore.attributes.handleClusterInvalidateUserPropertyValuesEpoch)
}
return
}
func (s LocalCacheStore) Reaction() store.ReactionStore {
return s.reaction
}
func (s LocalCacheStore) Role() store.RoleStore {
return s.role
}
func (s LocalCacheStore) Scheme() store.SchemeStore {
return s.scheme
}
func (s LocalCacheStore) FileInfo() store.FileInfoStore {
return s.fileInfo
}
func (s LocalCacheStore) Webhook() store.WebhookStore {
return s.webhook
}
func (s LocalCacheStore) Emoji() store.EmojiStore {
return s.emoji
}
func (s LocalCacheStore) Channel() store.ChannelStore {
return s.channel
}
func (s LocalCacheStore) Post() store.PostStore {
return s.post
}
func (s LocalCacheStore) TermsOfService() store.TermsOfServiceStore {
return s.termsOfService
}
func (s LocalCacheStore) User() store.UserStore {
return s.user
}
func (s LocalCacheStore) Team() store.TeamStore {
return s.team
}
func (s LocalCacheStore) AutoTranslation() store.AutoTranslationStore {
return s.autotranslation
}
func (s LocalCacheStore) ContentFlagging() store.ContentFlaggingStore {
return s.contentFlagging
}
func (s LocalCacheStore) DeliveryTracking() store.DeliveryTrackingStore {
return s.deliveryTracking
}
func (s LocalCacheStore) ReadReceipt() store.ReadReceiptStore {
return s.readReceipt
}
func (s LocalCacheStore) TemporaryPost() store.TemporaryPostStore {
return s.temporaryPost
}
func (s LocalCacheStore) SessionAttribute() store.SessionAttributeStore {
return &s.sessionAttribute
}
func (s LocalCacheStore) PropertyField() store.PropertyFieldStore {
return s.propertyField
}
func (s LocalCacheStore) AccessControlPolicy() store.AccessControlPolicyStore {
return s.accessControlPolicy
}
func (s LocalCacheStore) Attributes() store.AttributesStore {
return s.attributes
}
func (s LocalCacheStore) DropAllTables() {
s.Invalidate()
s.Store.DropAllTables()
}
func (s *LocalCacheStore) doInvalidateCacheCluster(cache cache.Cache, key string, props map[string]string) {
err := cache.Remove(key)
if err != nil {
s.logger.Warn("Error while removing cache entry", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
if s.cluster != nil && cache.GetInvalidateClusterEvent() != model.ClusterEventNone {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: []byte(key),
}
if props != nil {
msg.Props = props
}
s.cluster.SendClusterMessage(msg)
}
}
func (s *LocalCacheStore) doMultiInvalidateCacheCluster(cache cache.Cache, keys []string, props map[string]string) {
err := cache.RemoveMulti(keys)
if err != nil {
s.logger.Warn("Error while removing cache entry", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
if s.cluster != nil && cache.GetInvalidateClusterEvent() != model.ClusterEventNone {
for _, key := range keys {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: []byte(key),
}
if props != nil {
msg.Props = props
}
s.cluster.SendClusterMessage(msg)
}
}
}
func (s *LocalCacheStore) doStandardAddToCache(cache cache.Cache, key string, value any) {
err := cache.SetWithDefaultExpiry(key, value)
if err != nil {
s.logger.Warn("Error while setting cache entry", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
}
func (s *LocalCacheStore) doStandardAddToCacheWithExpiry(cache cache.Cache, key string, value any, ttl time.Duration) {
err := cache.SetWithExpiry(key, value, ttl)
if err != nil {
s.logger.Warn("Error while setting cache entry", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
}
func (s *LocalCacheStore) doStandardReadCache(c cache.Cache, key string, value any) error {
err := c.Get(key, value)
if err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter(c.Name())
}
return nil
}
if err != cache.ErrKeyNotFound {
s.logger.Warn("Error while reading from cache", mlog.Err(err), mlog.String("cache_name", c.Name()))
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter(c.Name())
}
return err
}
func (s *LocalCacheStore) doMultiReadCache(cache cache.Cache, keys []string, values []any) []error {
errs := cache.GetMulti(keys, values)
if s.metrics != nil {
for _, err := range errs {
if err == nil {
s.metrics.IncrementMemCacheHitCounter(cache.Name())
continue
}
s.metrics.IncrementMemCacheMissCounter(cache.Name())
}
}
return errs
}
func (s *LocalCacheStore) doIncrementCache(cache cache.ExternalCache, key string, val int) {
err := cache.Increment(key, val)
if err != nil {
s.logger.Warn("Error while incrementing cache entry", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
}
func (s *LocalCacheStore) doDecrementCache(cache cache.ExternalCache, key string, val int) {
err := cache.Decrement(key, val)
if err != nil {
s.logger.Warn("Error while decrementing cache entry", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
}
func (s *LocalCacheStore) doClearCacheCluster(cache cache.Cache) {
err := cache.Purge()
if err != nil {
s.logger.Warn("Error while purging cache", mlog.Err(err), mlog.String("cache_name", cache.Name()))
}
if s.cluster != nil && cache.GetInvalidateClusterEvent() != model.ClusterEventNone {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: clearCacheMessageData,
}
s.cluster.SendClusterMessage(msg)
}
}
func (s *LocalCacheStore) Invalidate() {
s.doClearCacheCluster(s.reactionCache)
s.doClearCacheCluster(s.schemeCache)
s.doClearCacheCluster(s.roleCache)
s.doClearCacheCluster(s.fileInfoCache)
s.doClearCacheCluster(s.webhookCache)
s.doClearCacheCluster(s.emojiCacheById)
s.doClearCacheCluster(s.emojiIdCacheByName)
s.doClearCacheCluster(s.channelMemberCountsCache)
s.doClearCacheCluster(s.channelPinnedPostCountsCache)
s.doClearCacheCluster(s.channelGuestCountCache)
s.doClearCacheCluster(s.channelByIdCache)
s.doClearCacheCluster(s.channelMembersForUserCache)
s.doClearCacheCluster(s.channelMembersNotifyPropsCache)
s.doClearCacheCluster(s.channelByNameCache)
s.doClearCacheCluster(s.postLastPostsCache)
s.doClearCacheCluster(s.termsOfServiceCache)
s.doClearCacheCluster(s.lastPostTimeCache)
s.doClearCacheCluster(s.userProfileByIdsCache)
s.doClearCacheCluster(s.allUserCache)
s.doClearCacheCluster(s.profilesInChannelCache)
s.doClearCacheCluster(s.teamAllTeamIdsForUserCache)
s.doClearCacheCluster(s.rolePermissionsCache)
s.doClearCacheCluster(s.userAutoTranslationCache)
s.doClearCacheCluster(s.postTranslationEtagCache)
s.doClearCacheCluster(s.deliveryTrackingCache)
s.doClearCacheCluster(s.readReceiptCache)
s.doClearCacheCluster(s.readReceiptPostReadersCache)
s.doClearCacheCluster(s.temporaryPostCache)
s.doClearCacheCluster(s.sessionAttributeCache)
s.doClearCacheCluster(s.propertyFieldCache)
s.doClearCacheCluster(s.accessControlPolicyEtagCache)
s.doClearCacheCluster(s.userPropertyValuesEpochCache)
}
// allocateCacheTargets is used to fill target value types
// for getting items from cache.
func allocateCacheTargets[T any](l int) []any {
toPass := make([]any, 0, l)
for range l {
toPass = append(toPass, new(T))
}
return toPass
}

View File

@@ -0,0 +1,167 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"sync"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
type storeType struct {
Name string
SqlSettings *model.SqlSettings
SqlStore *sqlstore.SqlStore
Store store.Store
}
var storeTypes []*storeType
func newStoreType(name, driver string) *storeType {
return &storeType{
Name: name,
SqlSettings: storetest.MakeSqlSettings(driver),
}
}
func StoreTest(t *testing.T, f func(*testing.T, request.CTX, store.Store)) {
defer func() {
if err := recover(); err != nil {
tearDownStores()
panic(err)
}
}()
for _, st := range storeTypes {
rctx := request.TestContext(t)
t.Run(st.Name, func(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
f(t, rctx, st.Store)
})
}
}
func StoreTestWithSqlStore(t *testing.T, f func(*testing.T, request.CTX, store.Store, storetest.SqlStore)) {
defer func() {
if err := recover(); err != nil {
tearDownStores()
panic(err)
}
}()
for _, st := range storeTypes {
rctx := request.TestContext(t)
t.Run(st.Name, func(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
f(t, rctx, st.Store, sqlstore.NewStoreTestWrapper(st.SqlStore))
})
}
}
func initStores(logger mlog.LoggerIFace) {
if testing.Short() {
return
}
storeTypes = append(storeTypes, newStoreType("LocalCache+PostgreSQL", model.DatabaseDriverPostgres))
defer func() {
if err := recover(); err != nil {
tearDownStores()
panic(err)
}
}()
var eg errgroup.Group
for _, st := range storeTypes {
eg.Go(func() error {
var err error
st.SqlStore, err = sqlstore.New(*st.SqlSettings, logger, nil, sqlstore.DisableMorphLogging())
if err != nil {
return err
}
st.Store, err = NewLocalCacheLayer(st.SqlStore, nil, nil, cache.NewProvider(), logger)
if err != nil {
return err
}
st.Store.DropAllTables()
st.Store.MarkSystemRanUnitTests()
return nil
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
}
var tearDownStoresOnce sync.Once
func tearDownStores() {
if testing.Short() {
return
}
tearDownStoresOnce.Do(func() {
var wg sync.WaitGroup
wg.Add(len(storeTypes))
for _, st := range storeTypes {
go func() {
if st.Store != nil {
st.Store.Close()
}
if st.SqlSettings != nil {
storetest.CleanupSqlSettings(st.SqlSettings)
}
wg.Done()
}()
}
wg.Wait()
})
}
func TestClearCacheCluster(t *testing.T) {
cluster := &testlib.FakeClusterInterface{}
lc := &LocalCacheStore{
cluster: cluster,
}
c := cache.NewLRU(&cache.CacheOptions{
Size: 10,
Name: "test",
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRoles,
})
lc.doClearCacheCluster(c)
assert.Len(t, cluster.GetMessages(), 1)
expectedMsg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForRoles,
SendType: model.ClusterSendBestEffort,
Data: clearCacheMessageData,
}
require.Equal(t, expectedMsg, cluster.GetMessages()[0])
c = cache.NewLRU(&cache.CacheOptions{
Size: 10,
Name: "test",
InvalidateClusterEvent: model.ClusterEventNone,
})
lc.doClearCacheCluster(c)
assert.Len(t, cluster.GetMessages(), 1)
}

View File

@@ -0,0 +1,269 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/mock"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
cachemocks "github.com/mattermost/mattermost/server/v8/platform/services/cache/mocks"
)
var mainHelper *testlib.MainHelper
func getMockCacheProvider() cache.Provider {
mockCacheProvider := cachemocks.Provider{}
// Each NewCache call returns a fresh LRU so that caches are isolated
// from each other, matching production behaviour.
call := mockCacheProvider.On("NewCache", mock.Anything)
call.Run(func(args mock.Arguments) {
call.ReturnArguments = mock.Arguments{cache.NewLRU(&cache.CacheOptions{Size: 128}), nil}
})
mockCacheProvider.On("Type").Return("lru")
return &mockCacheProvider
}
func getMockStore(t *testing.T) *mocks.Store {
mockStore := mocks.Store{}
fakeReaction := model.Reaction{PostId: "123"}
mockReactionsStore := mocks.ReactionStore{}
mockReactionsStore.On("Save", &fakeReaction).Return(&model.Reaction{}, nil)
mockReactionsStore.On("Delete", &fakeReaction).Return(&model.Reaction{}, nil)
mockReactionsStore.On("GetForPost", "123", false).Return([]*model.Reaction{&fakeReaction}, nil)
mockReactionsStore.On("GetForPost", "123", true).Return([]*model.Reaction{&fakeReaction}, nil)
mockStore.On("Reaction").Return(&mockReactionsStore)
mockAutoTranslationStore := mocks.AutoTranslationStore{}
// GetLatestPostUpdateAtForChannel now takes only channelID (no locale) since caching is per-channel
mockAutoTranslationStore.On("GetLatestPostUpdateAtForChannel", "channelId").Return(int64(5000), nil)
mockStore.On("AutoTranslation").Return(&mockAutoTranslationStore)
fakeRole := model.Role{Id: "123", Name: "role-name"}
fakeRole2 := model.Role{Id: "456", Name: "role-name2"}
mockRolesStore := mocks.RoleStore{}
mockRolesStore.On("Save", &fakeRole).Return(&model.Role{}, nil)
mockRolesStore.On("SavePreservingUnknownPermissions", &fakeRole).Return(&model.Role{}, nil)
mockRolesStore.On("Delete", "123").Return(&fakeRole, nil)
mockRolesStore.On("GetByName", mock.Anything, "role-name").Return(&fakeRole, nil)
mockRolesStore.On("GetByNames", []string{"role-name"}).Return([]*model.Role{&fakeRole}, nil)
mockRolesStore.On("GetByNames", []string{"role-name2"}).Return([]*model.Role{&fakeRole2}, nil)
mockRolesStore.On("PermanentDeleteAll").Return(nil)
mockStore.On("Role").Return(&mockRolesStore)
fakeScheme := model.Scheme{Id: "123", Name: "scheme-name"}
mockSchemesStore := mocks.SchemeStore{}
mockSchemesStore.On("Save", &fakeScheme).Return(&model.Scheme{}, nil)
mockSchemesStore.On("Delete", "123").Return(&model.Scheme{}, nil)
mockSchemesStore.On("Get", "123").Return(&fakeScheme, nil)
mockSchemesStore.On("PermanentDeleteAll").Return(nil)
mockStore.On("Scheme").Return(&mockSchemesStore)
fakeFileInfo := model.FileInfo{Id: "123", PostId: "123"}
mockFileInfoStore := mocks.FileInfoStore{}
mockFileInfoStore.On("GetForPost", "123", true, true, false).Return([]*model.FileInfo{&fakeFileInfo}, nil)
mockFileInfoStore.On("GetForPost", "123", true, true, true).Return([]*model.FileInfo{&fakeFileInfo}, nil)
mockFileInfoStore.On("GetByIds", []string{"123"}, true, false, false).Return([]*model.FileInfo{&fakeFileInfo}, nil)
mockStore.On("FileInfo").Return(&mockFileInfoStore)
fakeWebhook := model.IncomingWebhook{Id: "123"}
mockWebhookStore := mocks.WebhookStore{}
mockWebhookStore.On("GetIncoming", "123", true).Return(&fakeWebhook, nil)
mockWebhookStore.On("GetIncoming", "123", false).Return(&fakeWebhook, nil)
mockStore.On("Webhook").Return(&mockWebhookStore)
fakeEmoji := model.Emoji{Id: "123", Name: "name123"}
fakeEmoji2 := model.Emoji{Id: "321", Name: "name321"}
ctxEmoji := model.Emoji{Id: "master", Name: "name123"}
mockEmojiStore := mocks.EmojiStore{}
mockEmojiStore.On("Get", mock.Anything, "123", true).Return(&fakeEmoji, nil)
mockEmojiStore.On("Get", mock.Anything, "123", false).Return(&fakeEmoji, nil)
mockEmojiStore.On("Get", mock.IsType(&request.Context{}), "master", true).Return(&ctxEmoji, nil)
mockEmojiStore.On("Get", sqlstore.RequestContextWithMaster(request.TestContext(t)), "master", true).Return(&ctxEmoji, nil)
mockEmojiStore.On("GetByName", mock.Anything, "name123", true).Return(&fakeEmoji, nil)
mockEmojiStore.On("GetByName", mock.Anything, "name123", false).Return(&fakeEmoji, nil)
mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{"name123"}).Return([]*model.Emoji{&fakeEmoji}, nil)
mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{"name123", "name321"}).Return([]*model.Emoji{&fakeEmoji, &fakeEmoji2}, nil)
missingEmoji := model.Emoji{Id: "456", Name: "missing"}
// Master-routed lookups find the emoji, replica ones don't; registered
// first so it wins over the IsType matcher below.
mockEmojiStore.On("GetMultipleByName", mock.MatchedBy(func(rctx request.CTX) bool {
return sqlstore.HasMaster(rctx.Context())
}), []string{"missing"}).Return([]*model.Emoji{&missingEmoji}, nil)
mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{"missing"}).Return([]*model.Emoji{}, nil)
mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{"name123", "missing"}).Return([]*model.Emoji{&fakeEmoji}, nil)
mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{strings.Repeat("z", model.EmojiNameMaxLength+1)}).Return([]*model.Emoji{}, nil)
mockEmojiStore.On("GetByName", mock.Anything, "missing", true).Return(&missingEmoji, nil)
mockEmojiStore.On("Save", &missingEmoji).Return(&missingEmoji, nil)
mockEmojiStore.On("GetByName", mock.IsType(&request.Context{}), "master", true).Return(&ctxEmoji, nil)
mockEmojiStore.On("GetByName", sqlstore.RequestContextWithMaster(request.TestContext(t)), "master", false).Return(&ctxEmoji, nil)
mockEmojiStore.On("Delete", &fakeEmoji, int64(0)).Return(nil)
mockEmojiStore.On("Delete", &ctxEmoji, int64(0)).Return(nil)
mockStore.On("Emoji").Return(&mockEmojiStore)
mockCount := int64(10)
mockGuestCount := int64(12)
channelId := "channel1"
fakeChannel1 := model.Channel{Id: channelId, Name: "channel1-name"}
fakeChannel2 := model.Channel{Id: "channel2", Name: "channel2-name"}
mockChannelStore := mocks.ChannelStore{}
mockChannelStore.On("ClearCaches").Return()
mockChannelStore.On("Save", mock.IsType(&request.Context{}), &fakeChannel1, int64(0)).Return(&fakeChannel1, nil)
mockChannelStore.On("Update", mock.IsType(&request.Context{}), &fakeChannel1).Return(&fakeChannel1, nil)
mockChannelStore.On("GetMemberCount", "id", true).Return(mockCount, nil)
mockChannelStore.On("GetMemberCount", "id", false).Return(mockCount, nil)
mockChannelStore.On("GetGuestCount", "id", true).Return(mockGuestCount, nil)
mockChannelStore.On("GetGuestCount", "id", false).Return(mockGuestCount, nil)
mockChannelStore.On("Get", channelId, true).Return(&fakeChannel1, nil)
mockChannelStore.On("Get", channelId, false).Return(&fakeChannel1, nil)
mockChannelStore.On("GetMany", []string{channelId}, true).Return(model.ChannelList{&fakeChannel1}, nil)
mockChannelStore.On("GetMany", []string{channelId}, false).Return(model.ChannelList{&fakeChannel1}, nil)
mockChannelStore.On("GetMany", []string{fakeChannel2.Id}, true).Return(model.ChannelList{&fakeChannel2}, nil)
mockChannelStore.On("GetByNames", "team1", []string{fakeChannel1.Name}, true).Return([]*model.Channel{&fakeChannel1}, nil)
mockChannelStore.On("GetByNames", "team1", []string{fakeChannel2.Name}, true).Return([]*model.Channel{&fakeChannel2}, nil)
mockStore.On("Channel").Return(&mockChannelStore)
mockChannelsMemberCount := map[string]int64{
"channel1": 10,
"channel2": 20,
}
mockChannelStore.On("GetChannelsMemberCount", []string{"channel1", "channel2"}).Return(mockChannelsMemberCount, nil)
mockPinnedPostsCount := int64(10)
mockChannelStore.On("GetPinnedPostCount", "id", true).Return(mockPinnedPostsCount, nil)
mockChannelStore.On("GetPinnedPostCount", "id", false).Return(mockPinnedPostsCount, nil)
fakePosts := &model.PostList{}
fakeOptions := model.GetPostsOptions{ChannelId: "123", PerPage: 30}
mockPostStore := mocks.PostStore{}
mockPostStore.On("GetPosts", mock.AnythingOfType("*request.Context"), fakeOptions, true, map[string]bool{}).Return(fakePosts, nil)
mockPostStore.On("GetPosts", mock.AnythingOfType("*request.Context"), fakeOptions, false, map[string]bool{}).Return(fakePosts, nil)
mockPostStore.On("InvalidateLastPostTimeCache", "12360")
mockPostStoreOptions := model.GetPostsSinceOptions{
ChannelId: "channelId",
Time: 1,
SkipFetchThreads: false,
}
mockPostStoreEtagResult := fmt.Sprintf("%v.%v", model.CurrentVersion, 1)
mockPostStore.On("ClearCaches")
mockPostStore.On("InvalidateLastPostTimeCache", "channelId")
mockPostStore.On("GetEtag", "channelId", true, false, false).Return(mockPostStoreEtagResult)
mockPostStore.On("GetEtag", "channelId", false, false, false).Return(mockPostStoreEtagResult)
mockPostStore.On("GetEtag", "channelId", true, false, true).Return(mockPostStoreEtagResult)
mockPostStore.On("GetEtag", "channelId", false, false, true).Return(mockPostStoreEtagResult)
mockPostStore.On("GetPostsSince", mock.AnythingOfType("*request.Context"), mockPostStoreOptions, true, map[string]bool{}).Return(model.NewPostList(), nil)
mockPostStore.On("GetPostsSince", mock.AnythingOfType("*request.Context"), mockPostStoreOptions, false, map[string]bool{}).Return(model.NewPostList(), nil)
mockStore.On("Post").Return(&mockPostStore)
fakeTermsOfService := model.TermsOfService{Id: "123", CreateAt: 11111, UserId: "321", Text: "Terms of service test"}
mockTermsOfServiceStore := mocks.TermsOfServiceStore{}
mockTermsOfServiceStore.On("InvalidateTermsOfService", "123")
mockTermsOfServiceStore.On("Save", &fakeTermsOfService).Return(&fakeTermsOfService, nil)
mockTermsOfServiceStore.On("GetLatest", true).Return(&fakeTermsOfService, nil)
mockTermsOfServiceStore.On("GetLatest", false).Return(&fakeTermsOfService, nil)
mockTermsOfServiceStore.On("Get", "123", true).Return(&fakeTermsOfService, nil)
mockTermsOfServiceStore.On("Get", "123", false).Return(&fakeTermsOfService, nil)
mockStore.On("TermsOfService").Return(&mockTermsOfServiceStore)
fakeUser := []*model.User{{
Id: "123",
AuthData: new("authData"),
AuthService: "authService",
}}
mockUserStore := mocks.UserStore{}
mockUserStore.On("GetProfileByIds", mock.Anything, []string{"123"}, &store.UserGetByIdsOpts{}, true).Return(fakeUser, nil)
mockUserStore.On("GetProfileByIds", mock.Anything, []string{"123"}, &store.UserGetByIdsOpts{}, false).Return(fakeUser, nil)
fakeProfilesInChannelMap := map[string]*model.User{
"456": {Id: "456"},
}
mockUserStore.On("GetAllProfilesInChannel", mock.Anything, "123", true).Return(fakeProfilesInChannelMap, nil)
mockUserStore.On("GetAllProfilesInChannel", mock.Anything, "123", false).Return(fakeProfilesInChannelMap, nil)
mockUserStore.On("GetAllProfiles", mock.AnythingOfType("*model.UserGetOptions")).Return(fakeUser, nil)
mockUserStore.On("Get", mock.Anything, "123").Return(fakeUser[0], nil)
users := []*model.User{
fakeUser[0],
{
Id: "456",
AuthData: new("authData"),
AuthService: "authService",
},
}
mockUserStore.On("GetMany", mock.Anything, []string{"123", "456"}).Return(users, nil)
mockUserStore.On("GetMany", mock.Anything, []string{"123"}).Return(users[0:1], nil)
mockStore.On("User").Return(&mockUserStore)
fakeUserTeamIds := []string{"1", "2", "3"}
mockTeamStore := mocks.TeamStore{}
mockTeamStore.On("GetUserTeamIds", "123", true).Return(fakeUserTeamIds, nil)
mockTeamStore.On("GetUserTeamIds", "123", false).Return(fakeUserTeamIds, nil)
mockStore.On("Team").Return(&mockTeamStore)
mockContentFlaggingStore := mocks.ContentFlaggingStore{}
mockStore.On("ContentFlagging").Return(&mockContentFlaggingStore)
mockDeliveryTrackingStore := mocks.DeliveryTrackingStore{}
mockStore.On("DeliveryTracking").Return(&mockDeliveryTrackingStore)
mockSessionAttributeStore := mocks.SessionAttributeStore{}
mockStore.On("SessionAttribute").Return(&mockSessionAttributeStore)
fakeField := model.PropertyField{ID: "field-id", GroupID: "group-id", Name: "field-name"}
mockPropertyFieldStore := mocks.PropertyFieldStore{}
mockPropertyFieldStore.On("GetForGroup", mock.Anything, "group-id").Return([]*model.PropertyField{&fakeField}, nil)
mockPropertyFieldStore.On("Create", &fakeField).Return(&fakeField, nil)
mockPropertyFieldStore.On("Update", "group-id", []*model.PropertyField{&fakeField}, map[string]int64(nil)).Return([]*model.PropertyField{&fakeField}, nil)
mockPropertyFieldStore.On("Delete", "group-id", "field-id").Return(nil)
mockStore.On("PropertyField").Return(&mockPropertyFieldStore)
mockAccessControlPolicyStore := mocks.AccessControlPolicyStore{}
mockAccessControlPolicyStore.On("GetEtagEpoch", mock.Anything, "channel-id").Return("100-1", nil)
mockStore.On("AccessControlPolicy").Return(&mockAccessControlPolicyStore)
mockAttributesStore := mocks.AttributesStore{}
mockAttributesStore.On("GetUserPropertyValuesEpoch", mock.Anything, "user-id").Return("200-1", nil)
mockStore.On("Attributes").Return(&mockAttributesStore)
mockReadReceiptStore := &mocks.ReadReceiptStore{}
mockStore.On("ReadReceipt").Return(mockReadReceiptStore)
fakeTemporaryPost := model.TemporaryPost{
ID: "123",
Type: model.PostTypeBurnOnRead,
ExpireAt: model.GetMillis() + 300000,
Message: "test message",
FileIDs: []string{"file1"},
}
mockTemporaryPostStore := mocks.TemporaryPostStore{}
mockTemporaryPostStore.On("Save", mock.Anything, mock.AnythingOfType("*model.TemporaryPost")).Return(&fakeTemporaryPost, nil)
mockTemporaryPostStore.On("Delete", mock.Anything, "123").Return(nil)
mockTemporaryPostStore.On("Get", mock.Anything, "123", false).Return(&fakeTemporaryPost, nil)
mockTemporaryPostStore.On("Get", mock.Anything, "123", true).Return(&fakeTemporaryPost, nil)
mockTemporaryPostStore.On("InvalidateTemporaryPost", "123").Return()
mockStore.On("TemporaryPost").Return(&mockTemporaryPostStore)
return &mockStore
}
func TestMain(m *testing.M) {
mainHelper = testlib.NewMainHelperWithOptions(nil)
defer mainHelper.Close()
initStores(mainHelper.Logger)
mainHelper.Main(m)
tearDownStores()
}

View File

@@ -0,0 +1,179 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCachePostStore struct {
store.PostStore
rootStore *LocalCacheStore
}
func (s *LocalCachePostStore) handleClusterInvalidateLastPostTime(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.lastPostTimeCache.Purge()
} else {
s.rootStore.lastPostTimeCache.Remove(string(msg.Data))
}
}
func (s *LocalCachePostStore) handleClusterInvalidateLastPosts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postLastPostsCache.Purge()
} else {
s.rootStore.postLastPostsCache.Remove(string(msg.Data))
}
}
func (s *LocalCachePostStore) handleClusterInvalidatePostsUsage(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postsUsageCache.Purge()
} else {
s.rootStore.postsUsageCache.Remove(string(msg.Data))
}
}
func (s LocalCachePostStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.lastPostTimeCache)
s.rootStore.doClearCacheCluster(s.rootStore.postLastPostsCache)
s.rootStore.doClearCacheCluster(s.rootStore.postsUsageCache)
s.PostStore.ClearCaches()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.lastPostTimeCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.postLastPostsCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.postsUsageCache.Name())
}
}
func (s LocalCachePostStore) InvalidateLastPostTimeCache(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.lastPostTimeCache, channelId, nil)
// Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"30", nil)
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"60", nil)
s.PostStore.InvalidateLastPostTimeCache(channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.lastPostTimeCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.postLastPostsCache.Name())
}
}
func (s LocalCachePostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool, includeTranslations bool) string {
var baseEtag string
if allowFromCache {
var lastTime int64
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, channelId, &lastTime); err == nil {
baseEtag = fmt.Sprintf("%v.%v", model.CurrentVersion, lastTime)
}
}
if baseEtag == "" {
baseEtag = s.PostStore.GetEtag(channelId, allowFromCache, collapsedThreads, includeTranslations)
splittedResult := strings.Split(baseEtag, ".")
lastTime, _ := strconv.ParseInt((splittedResult[len(splittedResult)-1]), 10, 64)
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, channelId, lastTime)
}
// If translations should be included, append translation time
if includeTranslations {
translationTime, err := s.rootStore.AutoTranslation().GetLatestPostUpdateAtForChannel(channelId)
if err == nil && translationTime > 0 {
return fmt.Sprintf("%s_%d", baseEtag, translationTime)
}
}
return baseEtag
}
func (s LocalCachePostStore) GetPostsSince(rctx request.CTX, options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if allowFromCache {
// If the last post in the channel's time is less than or equal to the time we are getting posts since,
// we can safely return no posts.
var lastTime int64
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, options.ChannelId, &lastTime); err == nil && lastTime <= options.Time {
list := model.NewPostList()
return list, nil
}
}
list, err := s.PostStore.GetPostsSince(rctx, options, allowFromCache, sanitizeOptions)
latestUpdate := options.Time
if err == nil {
for _, p := range list.ToSlice() {
if latestUpdate < p.UpdateAt {
latestUpdate = p.UpdateAt
}
}
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, options.ChannelId, latestUpdate)
}
return list, err
}
func (s LocalCachePostStore) GetPosts(rctx request.CTX, options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if !allowFromCache {
return s.PostStore.GetPosts(rctx, options, allowFromCache, sanitizeOptions)
}
offset := options.PerPage * options.Page
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
var cacheItem *model.PostList
if err := s.rootStore.doStandardReadCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), &cacheItem); err == nil {
return cacheItem, nil
}
}
list, err := s.PostStore.GetPosts(rctx, options, false, sanitizeOptions)
if err != nil {
return nil, err
}
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
s.rootStore.doStandardAddToCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), list)
}
return list, err
}
// AnalyticsPostCount looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy.
func (s LocalCachePostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
if !options.AllowFromCache || options.MustHaveFile || options.MustHaveHashtag || !options.UsersPostsOnly || !options.ExcludeDeleted || options.TeamId != "" {
return s.PostStore.AnalyticsPostCount(options)
}
// Currently cache only for app > usage > GetPostsUsage()
// Other filter combinations can be cached if required
cacheKey := "posts_usage"
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.postsUsageCache, cacheKey, &count); err == nil {
return count, nil
}
count, err := s.PostStore.AnalyticsPostCount(options)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.postsUsageCache, cacheKey, count)
return count, nil
}

View File

@@ -0,0 +1,256 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestPostStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestPostStore)
}
func TestPostStoreLastPostTimeCache(t *testing.T) {
rctx := request.TestContext(t)
var fakeLastTime int64 = 1
channelId := "channelId"
fakeOptions := model.GetPostsSinceOptions{
ChannelId: channelId,
Time: fakeLastTime,
SkipFetchThreads: false,
}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("GetEtag: first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
expectedResult := fmt.Sprintf("%v.%v", model.CurrentVersion, fakeLastTime)
etag := cachedStore.Post().GetEtag(channelId, true, false, false)
assert.Equal(t, etag, expectedResult)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
etag = cachedStore.Post().GetEtag(channelId, true, false, false)
assert.Equal(t, etag, expectedResult)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
})
t.Run("GetEtag: first call not cached, second force no cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Post().GetEtag(channelId, true, false, false)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
cachedStore.Post().GetEtag(channelId, false, false, false)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 2)
})
t.Run("GetEtag: first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Post().GetEtag(channelId, true, false, false)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
cachedStore.Post().InvalidateLastPostTimeCache(channelId)
cachedStore.Post().GetEtag(channelId, true, false, false)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 2)
})
t.Run("GetEtag: first call not cached, clear caches, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Post().GetEtag(channelId, true, false, false)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
cachedStore.Post().ClearCaches()
cachedStore.Post().GetEtag(channelId, true, false, false)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 2)
})
t.Run("GetPostsSince: first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
expectedResult := model.NewPostList()
list, err := cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, list, expectedResult)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
list, err = cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, list, expectedResult)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
})
t.Run("GetPostsSince: first call not cached, second force no cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
cachedStore.Post().GetPostsSince(rctx, fakeOptions, false, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 2)
})
t.Run("GetPostsSince: first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
cachedStore.Post().InvalidateLastPostTimeCache(channelId)
cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 2)
})
t.Run("GetPostsSince: first call not cached, clear caches, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
cachedStore.Post().ClearCaches()
cachedStore.Post().GetPostsSince(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 2)
})
t.Run("GetEtag: with includeTranslations appends translation time", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// Expected: "{version}.{lastTime}.{translationTime}"
expectedResult := fmt.Sprintf("%v.%v_%v", model.CurrentVersion, fakeLastTime, 5000)
etag := cachedStore.Post().GetEtag(channelId, true, false, true)
assert.Equal(t, expectedResult, etag)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
mockStore.AutoTranslation().(*mocks.AutoTranslationStore).AssertNumberOfCalls(t, "GetLatestPostUpdateAtForChannel", 1)
})
t.Run("GetEtag: translation etag is cached per-channel", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First call with includeTranslations=true - should call GetLatestPostUpdateAtForChannel
expected := fmt.Sprintf("%v.%v_%v", model.CurrentVersion, fakeLastTime, 5000)
etag := cachedStore.Post().GetEtag(channelId, true, false, true)
assert.Equal(t, expected, etag)
mockStore.AutoTranslation().(*mocks.AutoTranslationStore).AssertNumberOfCalls(t, "GetLatestPostUpdateAtForChannel", 1)
// Second call - should use cached translation time
etag = cachedStore.Post().GetEtag(channelId, true, false, true)
assert.Equal(t, expected, etag)
mockStore.AutoTranslation().(*mocks.AutoTranslationStore).AssertNumberOfCalls(t, "GetLatestPostUpdateAtForChannel", 1)
})
t.Run("GetEtag: invalidate translation etag clears the channel cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// Call GetEtag with includeTranslations=true
expected := fmt.Sprintf("%v.%v_%v", model.CurrentVersion, fakeLastTime, 5000)
etag := cachedStore.Post().GetEtag(channelId, true, false, true)
assert.Equal(t, expected, etag)
mockStore.AutoTranslation().(*mocks.AutoTranslationStore).AssertNumberOfCalls(t, "GetLatestPostUpdateAtForChannel", 1)
// Invalidate the channel's post translation etag
cachedStore.AutoTranslation().InvalidatePostTranslationEtag(channelId)
// Call GetEtag again - should re-fetch
etag = cachedStore.Post().GetEtag(channelId, true, false, true)
assert.Equal(t, expected, etag)
mockStore.AutoTranslation().(*mocks.AutoTranslationStore).AssertNumberOfCalls(t, "GetLatestPostUpdateAtForChannel", 2)
})
}
func TestPostStoreCache(t *testing.T) {
rctx := request.TestContext(t)
fakePosts := &model.PostList{}
fakeOptions := model.GetPostsOptions{ChannelId: "123", PerPage: 30}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotPosts, err := cachedStore.Post().GetPosts(rctx, fakeOptions, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, fakePosts, gotPosts)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
_, _ = cachedStore.Post().GetPosts(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotPosts, err := cachedStore.Post().GetPosts(rctx, fakeOptions, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, fakePosts, gotPosts)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
_, _ = cachedStore.Post().GetPosts(rctx, fakeOptions, false, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 2)
})
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotPosts, err := cachedStore.Post().GetPosts(rctx, fakeOptions, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, fakePosts, gotPosts)
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
cachedStore.Post().InvalidateLastPostTimeCache("12360")
_, _ = cachedStore.Post().GetPosts(rctx, fakeOptions, true, map[string]bool{})
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
})
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func (s LocalCachePropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
created, err := s.PropertyFieldStore.Create(field)
if err != nil {
return nil, err
}
s.InvalidateFieldsForGroup(created.GroupID)
return created, nil
}
func (s LocalCachePropertyFieldStore) Update(groupID string, fields []*model.PropertyField, expectedUpdateAts map[string]int64) ([]*model.PropertyField, error) {
updated, err := s.PropertyFieldStore.Update(groupID, fields, expectedUpdateAts)
if err != nil {
return nil, err
}
// The returned slice includes both the requested fields and any linked
// fields the store propagated to. Invalidate each distinct group so all
// affected GetForGroup caches are cleared.
invalidated := make(map[string]bool, len(updated))
for _, field := range updated {
if invalidated[field.GroupID] {
continue
}
invalidated[field.GroupID] = true
s.InvalidateFieldsForGroup(field.GroupID)
}
return updated, nil
}
func (s LocalCachePropertyFieldStore) Delete(groupID string, id string) error {
if err := s.PropertyFieldStore.Delete(groupID, id); err != nil {
return err
}
s.InvalidateFieldsForGroup(groupID)
return nil
}
type LocalCachePropertyFieldStore struct {
store.PropertyFieldStore
rootStore *LocalCacheStore
}
func (s *LocalCachePropertyFieldStore) handleClusterInvalidatePropertyField(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
if err := s.rootStore.propertyFieldCache.Purge(); err != nil {
s.rootStore.logger.Warn("failed to purge property field cache", mlog.Err(err))
}
} else if err := s.rootStore.propertyFieldCache.Remove(string(msg.Data)); err != nil {
s.rootStore.logger.Warn("failed to remove property field cache entry", mlog.Err(err))
}
}
func (s LocalCachePropertyFieldStore) InvalidateFieldsForGroup(groupID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.propertyFieldCache, groupID, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.propertyFieldCache.Name())
}
}
func (s *LocalCachePropertyFieldStore) getFieldsForGroupFromCache(groupID string) ([]*model.PropertyField, bool) {
var fields []*model.PropertyField
if err := s.rootStore.doStandardReadCache(s.rootStore.propertyFieldCache, groupID, &fields); err == nil {
return fields, true
}
return nil, false
}
func (s LocalCachePropertyFieldStore) GetForGroup(rctx request.CTX, groupID string) ([]*model.PropertyField, error) {
if fields, ok := s.getFieldsForGroupFromCache(groupID); ok {
return fields, nil
}
fields, err := s.PropertyFieldStore.GetForGroup(rctx, groupID)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.propertyFieldCache, groupID, fields)
return fields, nil
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestPropertyFieldStoreCache(t *testing.T) {
groupID := "group-id"
fakeField := model.PropertyField{ID: "field-id", GroupID: groupID, Name: "field-name"}
fakeFields := []*model.PropertyField{&fakeField}
logger := mlog.CreateConsoleTestLogger(t)
rctx := request.TestContext(t)
t.Run("GetForGroup cached on second call", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
fields, err := cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
assert.Equal(t, fakeFields, fields)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 1)
fields, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
assert.Equal(t, fakeFields, fields)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 1)
})
t.Run("Create invalidates the group cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 1)
_, err = cachedStore.PropertyField().Create(&fakeField)
require.NoError(t, err)
_, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 2)
})
t.Run("Update invalidates the group cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 1)
_, err = cachedStore.PropertyField().Update(groupID, fakeFields, nil)
require.NoError(t, err)
_, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 2)
})
t.Run("Delete invalidates the group cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 1)
err = cachedStore.PropertyField().Delete(groupID, fakeField.ID)
require.NoError(t, err)
_, err = cachedStore.PropertyField().GetForGroup(rctx, groupID)
require.NoError(t, err)
mockStore.PropertyField().(*mocks.PropertyFieldStore).AssertNumberOfCalls(t, "GetForGroup", 2)
})
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheReactionStore struct {
store.ReactionStore
rootStore *LocalCacheStore
}
func (s *LocalCacheReactionStore) handleClusterInvalidateReaction(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.reactionCache.Purge()
} else {
s.rootStore.reactionCache.Remove(string(msg.Data))
}
}
func (s LocalCacheReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId, nil)
return s.ReactionStore.Save(reaction)
}
func (s LocalCacheReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId, nil)
return s.ReactionStore.Delete(reaction)
}
func (s LocalCacheReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
if !allowFromCache {
return s.ReactionStore.GetForPost(postId, false)
}
var reaction []*model.Reaction
if err := s.rootStore.doStandardReadCache(s.rootStore.reactionCache, postId, &reaction); err == nil {
return reaction, nil
}
reaction, err := s.ReactionStore.GetForPost(postId, false)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.reactionCache, postId, reaction)
return reaction, nil
}
func (s LocalCacheReactionStore) DeleteAllWithEmojiName(rctx request.CTX, emojiName string) error {
// This could be improved. Right now we just clear the whole
// cache because we don't have a way find what post Ids have this emoji name.
defer s.rootStore.doClearCacheCluster(s.rootStore.reactionCache)
return s.ReactionStore.DeleteAllWithEmojiName(rctx, emojiName)
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestReactionStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestReactionStore)
}
func TestReactionStoreCache(t *testing.T) {
fakeReaction := model.Reaction{PostId: "123"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
reaction, err := cachedStore.Reaction().GetForPost("123", true)
require.NoError(t, err)
assert.Equal(t, reaction, []*model.Reaction{&fakeReaction})
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
assert.Equal(t, reaction, []*model.Reaction{&fakeReaction})
cachedStore.Reaction().GetForPost("123", true)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Reaction().GetForPost("123", true)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
cachedStore.Reaction().GetForPost("123", false)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 2)
})
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Reaction().GetForPost("123", true)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
cachedStore.Reaction().Save(&fakeReaction)
cachedStore.Reaction().GetForPost("123", true)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 2)
})
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Reaction().GetForPost("123", true)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
cachedStore.Reaction().Delete(&fakeReaction)
cachedStore.Reaction().GetForPost("123", true)
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 2)
})
}

View File

@@ -0,0 +1,188 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"fmt"
"slices"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
type LocalCacheReadReceiptStore struct {
store.ReadReceiptStore
rootStore *LocalCacheStore
}
func (s *LocalCacheReadReceiptStore) handleClusterInvalidateReadReceipts(msg *model.ClusterMessage) {
if err := s.rootStore.readReceiptCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge read receipt cache", mlog.Err(err))
}
}
func (s LocalCacheReadReceiptStore) ClearCaches() {
if err := s.rootStore.readReceiptCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge read receipt cache", mlog.Err(err))
}
if err := s.rootStore.readReceiptPostReadersCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge read receipt post readers cache", mlog.Err(err))
}
if err := s.rootStore.readReceiptPostUnreadCountCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge read receipt post unread count cache", mlog.Err(err))
}
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.readReceiptCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.readReceiptPostReadersCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.readReceiptPostUnreadCountCache.Name())
}
}
func (s LocalCacheReadReceiptStore) InvalidateReadReceiptForPostsCache(postID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.readReceiptPostReadersCache, postID, nil)
s.rootStore.doInvalidateCacheCluster(s.rootStore.readReceiptPostUnreadCountCache, postID, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.readReceiptPostReadersCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.readReceiptPostUnreadCountCache.Name())
}
if externalCache, ok := s.rootStore.readReceiptCache.(cache.ExternalCache); ok {
// For redis, invalidate all keys with pattern "postID:*"
s.rootStore.doInvalidateCacheCluster(externalCache, fmt.Sprintf("%s:*", postID), nil)
} else {
if err := s.rootStore.readReceiptCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge read receipt cache", mlog.Err(err))
return
}
}
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.readReceiptCache.Name())
}
}
func (s LocalCacheReadReceiptStore) Delete(rctx request.CTX, postID, userID string) error {
defer func() {
s.InvalidateReadReceiptForPostsCache(postID)
}()
return s.ReadReceiptStore.Delete(rctx, postID, userID)
}
func (s LocalCacheReadReceiptStore) DeleteByPost(rctx request.CTX, postID string) error {
defer func(postID string) {
s.InvalidateReadReceiptForPostsCache(postID)
}(postID)
return s.ReadReceiptStore.DeleteByPost(rctx, postID)
}
func (s LocalCacheReadReceiptStore) Save(rctx request.CTX, receipt *model.ReadReceipt) (*model.ReadReceipt, error) {
defer func() {
s.rootStore.doInvalidateCacheCluster(s.rootStore.readReceiptCache, fmt.Sprintf("%s:%s", receipt.PostID, receipt.UserID), nil)
s.rootStore.doInvalidateCacheCluster(s.rootStore.readReceiptPostReadersCache, receipt.PostID, nil)
s.rootStore.doInvalidateCacheCluster(s.rootStore.readReceiptPostUnreadCountCache, receipt.PostID, nil)
}()
return s.ReadReceiptStore.Save(rctx, receipt)
}
func (s LocalCacheReadReceiptStore) Update(rctx request.CTX, receipt *model.ReadReceipt) (*model.ReadReceipt, error) {
defer func() {
s.rootStore.doInvalidateCacheCluster(s.rootStore.readReceiptCache, fmt.Sprintf("%s:%s", receipt.PostID, receipt.UserID), nil)
}()
return s.ReadReceiptStore.Update(rctx, receipt)
}
func (s LocalCacheReadReceiptStore) Get(rctx request.CTX, postID, userID string) (*model.ReadReceipt, error) {
// no need to store the entire struct in cache, just the expireAt would be sufficient
// as other two fields are part of the cache key
var expireAt int64
if err := s.rootStore.doStandardReadCache(s.rootStore.readReceiptCache, fmt.Sprintf("%s:%s", postID, userID), &expireAt); err == nil {
return &model.ReadReceipt{
PostID: postID,
UserID: userID,
ExpireAt: expireAt,
}, nil
}
rr, err := s.ReadReceiptStore.Get(rctx, postID, userID)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.readReceiptCache, fmt.Sprintf("%s:%s", postID, userID), rr.ExpireAt)
// Update post readers cache: add this userID to the list if not already present
var existingUserIDs []string
if err := s.rootStore.doStandardReadCache(s.rootStore.readReceiptPostReadersCache, postID, &existingUserIDs); err == nil {
// Cache exists: check if userID is already in the list
if !slices.Contains(existingUserIDs, userID) {
// Add userID to the existing list
existingUserIDs = append(existingUserIDs, userID)
s.rootStore.doStandardAddToCache(s.rootStore.readReceiptPostReadersCache, postID, existingUserIDs)
}
} else {
// Cache doesn't exist: create new entry with just this userID
s.rootStore.doStandardAddToCache(s.rootStore.readReceiptPostReadersCache, postID, []string{userID})
}
return rr, nil
}
func (s LocalCacheReadReceiptStore) GetByPost(rctx request.CTX, postID string) ([]*model.ReadReceipt, error) {
// Try to get cached user IDs for this post
var cachedUserIDs []string
if err := s.rootStore.doStandardReadCache(s.rootStore.readReceiptPostReadersCache, postID, &cachedUserIDs); err == nil {
// Cache hit: reconstruct receipts from cached user IDs and individual receipt caches
receipts := make([]*model.ReadReceipt, 0, len(cachedUserIDs))
for _, userID := range cachedUserIDs {
var expireAt int64
if err := s.rootStore.doStandardReadCache(s.rootStore.readReceiptCache, fmt.Sprintf("%s:%s", postID, userID), &expireAt); err == nil {
receipts = append(receipts, &model.ReadReceipt{
PostID: postID,
UserID: userID,
ExpireAt: expireAt,
})
}
}
// If we got all receipts from cache, return them
if len(receipts) == len(cachedUserIDs) {
return receipts, nil
}
}
// Cache miss or partial cache: fetch from underlying store
receipts, err := s.ReadReceiptStore.GetByPost(rctx, postID)
if err != nil {
return nil, err
}
// Cache the user IDs for this post
userIDs := make([]string, len(receipts))
for i, receipt := range receipts {
userIDs[i] = receipt.UserID
// Also ensure individual receipts are cached
s.rootStore.doStandardAddToCache(s.rootStore.readReceiptCache, fmt.Sprintf("%s:%s", postID, receipt.UserID), receipt.ExpireAt)
}
s.rootStore.doStandardAddToCache(s.rootStore.readReceiptPostReadersCache, postID, userIDs)
return receipts, nil
}
func (s LocalCacheReadReceiptStore) GetUnreadCountForPost(rctx request.CTX, post *model.Post) (int64, error) {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.readReceiptPostUnreadCountCache, post.Id, &count); err == nil {
return count, nil
}
count, err := s.ReadReceiptStore.GetUnreadCountForPost(rctx, post)
if err != nil {
return count, err
}
s.rootStore.doStandardAddToCache(s.rootStore.readReceiptPostUnreadCountCache, post.Id, count)
return count, nil
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestReadReceiptStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestReadReceiptStore)
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"slices"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
type LocalCacheRoleStore struct {
store.RoleStore
rootStore *LocalCacheStore
}
func (s *LocalCacheRoleStore) handleClusterInvalidateRole(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.roleCache.Purge()
} else {
s.rootStore.roleCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheRoleStore) handleClusterInvalidateRolePermissions(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.rolePermissionsCache.Purge()
} else {
s.rootStore.rolePermissionsCache.Remove(string(msg.Data))
}
}
func (s LocalCacheRoleStore) Save(role *model.Role) (*model.Role, error) {
if role.Name != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name, nil)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return s.RoleStore.Save(role)
}
func (s LocalCacheRoleStore) SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error) {
if role.Name != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name, nil)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return s.RoleStore.SavePreservingUnknownPermissions(role)
}
func (s LocalCacheRoleStore) GetByName(rctx request.CTX, name string) (*model.Role, error) {
var role *model.Role
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, name, &role); err == nil {
return role, nil
}
role, err := s.RoleStore.GetByName(rctx, name)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, name, role)
return role, nil
}
func (s LocalCacheRoleStore) GetByNames(names []string) ([]*model.Role, error) {
var foundRoles []*model.Role
var rolesToQuery []string
toPass := allocateCacheTargets[*model.Role](len(names))
errs := s.rootStore.doMultiReadCache(s.rootStore.roleCache, names, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
s.rootStore.logger.Warn("Error in Rolestore.GetByNames: ", mlog.Err(err))
}
rolesToQuery = append(rolesToQuery, names[i])
} else {
gotRole := *(toPass[i].(**model.Role))
if gotRole != nil {
foundRoles = append(foundRoles, gotRole)
continue
}
s.rootStore.logger.Warn("Found nil role in GetByNames. This is not expected")
}
}
if len(rolesToQuery) == 0 {
return foundRoles, nil
}
roles, err := s.RoleStore.GetByNames(rolesToQuery)
if err != nil {
return nil, err
}
for _, role := range roles {
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, role.Name, role)
}
return append(foundRoles, roles...), nil
}
func (s LocalCacheRoleStore) Delete(roleId string) (*model.Role, error) {
role, err := s.RoleStore.Delete(roleId)
if err == nil {
s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name, nil)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return role, err
}
func (s LocalCacheRoleStore) PermanentDeleteAll() error {
defer s.rootStore.roleCache.Purge()
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.RoleStore.PermanentDeleteAll()
}
func (s LocalCacheRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
slices.Sort(roleNames)
cacheKey := strings.Join(roleNames, "/")
var rolePermissionsMap map[string]*model.RolePermissions
if err := s.rootStore.doStandardReadCache(s.rootStore.rolePermissionsCache, cacheKey, &rolePermissionsMap); err == nil {
return rolePermissionsMap, nil
}
rolePermissionsMap, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.rolePermissionsCache, cacheKey, rolePermissionsMap)
return rolePermissionsMap, nil
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestRoleStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestRoleStore)
}
func TestRoleStoreCache(t *testing.T) {
fakeRole := model.Role{Id: "123", Name: "role-name"}
logger := mlog.CreateConsoleTestLogger(t)
rctx := request.TestContext(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
role, err := cachedStore.Role().GetByName(rctx, "role-name")
require.NoError(t, err)
assert.Equal(t, role, &fakeRole)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
require.NoError(t, err)
assert.Equal(t, role, &fakeRole)
cachedStore.Role().GetByName(rctx, "role-name")
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
})
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.Role().GetByName(rctx, "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
_, err = cachedStore.Role().Save(&fakeRole)
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "Save", 1)
_, err = cachedStore.Role().GetByName(rctx, "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("first call not cached, save preserving unknown permissions, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.Role().GetByName(rctx, "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
_, err = cachedStore.Role().SavePreservingUnknownPermissions(&fakeRole)
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "SavePreservingUnknownPermissions", 1)
_, err = cachedStore.Role().GetByName(rctx, "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Role().GetByName(rctx, "role-name")
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Role().Delete("123")
cachedStore.Role().GetByName(rctx, "role-name")
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("first call not cached, permanent delete all, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Role().GetByName(rctx, "role-name")
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Role().PermanentDeleteAll()
cachedStore.Role().GetByName(rctx, "role-name")
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
})
}
func TestRoleStoreGetByNamesCache(t *testing.T) {
fakeRole := model.Role{Id: "123", Name: "role-name"}
fakeRole2 := model.Role{Id: "456", Name: "role-name2"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
roles, err := cachedStore.Role().GetByNames([]string{"role-name"})
require.NoError(t, err)
assert.Equal(t, roles[0], &fakeRole)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByNames", 1)
roles, err = cachedStore.Role().GetByNames([]string{"role-name"})
require.NoError(t, err)
assert.Equal(t, roles[0], &fakeRole)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByNames", 1)
roles, err = cachedStore.Role().GetByNames([]string{"role-name", "role-name2"})
require.NoError(t, err)
assert.ElementsMatch(t, []*model.Role{&fakeRole, &fakeRole2}, roles)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByNames", 2)
})
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheSchemeStore struct {
store.SchemeStore
rootStore *LocalCacheStore
}
func (s *LocalCacheSchemeStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.schemeCache.Purge()
} else {
s.rootStore.schemeCache.Remove(string(msg.Data))
}
}
func (s LocalCacheSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
if scheme.Id != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, scheme.Id, nil)
}
return s.SchemeStore.Save(scheme)
}
func (s LocalCacheSchemeStore) Get(schemeId string) (*model.Scheme, error) {
var scheme *model.Scheme
if err := s.rootStore.doStandardReadCache(s.rootStore.schemeCache, schemeId, &scheme); err == nil {
return scheme, nil
}
scheme, err := s.SchemeStore.Get(schemeId)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.schemeCache, schemeId, scheme)
return scheme, nil
}
func (s LocalCacheSchemeStore) Delete(schemeId string) (*model.Scheme, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, schemeId, nil)
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.channelByIdCache)
return s.SchemeStore.Delete(schemeId)
}
func (s LocalCacheSchemeStore) PermanentDeleteAll() error {
defer s.rootStore.doClearCacheCluster(s.rootStore.schemeCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.channelByIdCache)
return s.SchemeStore.PermanentDeleteAll()
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestSchemeStore(t *testing.T) {
StoreTest(t, storetest.TestSchemeStore)
}
func TestSchemeStoreCache(t *testing.T) {
fakeScheme := model.Scheme{Id: "123", Name: "scheme-name"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
scheme, err := cachedStore.Scheme().Get("123")
require.NoError(t, err)
assert.Equal(t, scheme, &fakeScheme)
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
require.NoError(t, err)
assert.Equal(t, scheme, &fakeScheme)
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Scheme().Save(&fakeScheme)
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Scheme().Delete("123")
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call not cached, permanent delete all, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.Scheme().PermanentDeleteAll()
cachedStore.Scheme().Get("123")
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 2)
})
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"maps"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
type sessionAttributeEntry struct {
Attrs map[string]any
Timestamps map[string]int64
}
type LocalCacheSessionAttributeStore struct {
store.SessionAttributeStore
rootStore *LocalCacheStore
}
func (s *LocalCacheSessionAttributeStore) handleClusterInvalidateSessionAttributes(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
err := s.rootStore.sessionAttributeCache.Purge()
if err != nil {
s.rootStore.logger.Warn("Error while purging session attribute cache", mlog.Err(err))
}
} else {
err := s.rootStore.sessionAttributeCache.Remove(string(msg.Data))
if err != nil {
s.rootStore.logger.Warn("Error while removing session attribute cache", mlog.Err(err))
}
}
}
func (s *LocalCacheSessionAttributeStore) Refresh(sessionID string, attrs map[string]any, updatedAt int64) error {
if len(attrs) == 0 {
return nil
}
entry := &sessionAttributeEntry{
Attrs: make(map[string]any, len(attrs)),
Timestamps: make(map[string]int64, len(attrs)),
}
var existing *sessionAttributeEntry
if err := s.rootStore.doStandardReadCache(s.rootStore.sessionAttributeCache, sessionID, &existing); err == nil && existing != nil {
maps.Copy(entry.Attrs, existing.Attrs)
maps.Copy(entry.Timestamps, existing.Timestamps)
}
for key, value := range attrs {
if entry.Timestamps[key] > updatedAt {
continue
}
entry.Attrs[key] = value
entry.Timestamps[key] = updatedAt
}
s.rootStore.doStandardAddToCache(s.rootStore.sessionAttributeCache, sessionID, entry)
return nil
}
func (s *LocalCacheSessionAttributeStore) Invalidate(sessionID string) error {
s.rootStore.doInvalidateCacheCluster(s.rootStore.sessionAttributeCache, sessionID, nil)
return nil
}
func (s *LocalCacheSessionAttributeStore) Clear() error {
s.rootStore.doClearCacheCluster(s.rootStore.sessionAttributeCache)
return nil
}
func (s *LocalCacheSessionAttributeStore) Get(sessionID string) (map[string]any, map[string]int64, error) {
var entry *sessionAttributeEntry
if err := s.rootStore.doStandardReadCache(s.rootStore.sessionAttributeCache, sessionID, &entry); err != nil {
return nil, nil, err
}
if entry == nil {
return nil, nil, cache.ErrKeyNotFound
}
return entry.Attrs, entry.Timestamps, nil
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
func TestSessionAttributeStoreCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("Refresh writes attributes that Get returns", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionID := model.NewId()
err = cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Chrome",
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
}, model.GetMillis())
require.NoError(t, err)
got, _, err := cachedStore.SessionAttribute().Get(sessionID)
require.NoError(t, err)
require.Equal(t, "Chrome", got[model.SessionAttributesPropertyFieldUserAgentBrowserName])
require.Equal(t, "192.0.2.10", got[model.SessionAttributesPropertyFieldIPAddress])
})
t.Run("Refresh merges partial attributes", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionID := model.NewId()
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Chrome",
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Firefox",
}, model.GetMillis()))
got, timestamps, err := cachedStore.SessionAttribute().Get(sessionID)
require.NoError(t, err)
require.Equal(t, "Firefox", got[model.SessionAttributesPropertyFieldUserAgentBrowserName])
require.Equal(t, "192.0.2.10", got[model.SessionAttributesPropertyFieldIPAddress])
require.NotZero(t, timestamps[model.SessionAttributesPropertyFieldUserAgentBrowserName])
require.NotZero(t, timestamps[model.SessionAttributesPropertyFieldIPAddress])
})
t.Run("Get on missing session returns ErrKeyNotFound", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
got, _, err := cachedStore.SessionAttribute().Get(model.NewId())
require.ErrorIs(t, err, cache.ErrKeyNotFound)
require.Nil(t, got)
})
t.Run("cluster invalidation with session id removes single entry", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionA := model.NewId()
sessionB := model.NewId()
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionA, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionB, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "203.0.113.42",
}, model.GetMillis()))
cachedStore.sessionAttribute.handleClusterInvalidateSessionAttributes(&model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForSessionAttributes,
Data: []byte(sessionA),
})
_, _, err = cachedStore.SessionAttribute().Get(sessionA)
require.ErrorIs(t, err, cache.ErrKeyNotFound)
got, _, err := cachedStore.SessionAttribute().Get(sessionB)
require.NoError(t, err)
require.Equal(t, "203.0.113.42", got[model.SessionAttributesPropertyFieldIPAddress])
})
t.Run("Invalidate removes only the given session entry", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionA := model.NewId()
sessionB := model.NewId()
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionA, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionB, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "203.0.113.42",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Invalidate(sessionA))
_, _, err = cachedStore.SessionAttribute().Get(sessionA)
require.ErrorIs(t, err, cache.ErrKeyNotFound)
got, _, err := cachedStore.SessionAttribute().Get(sessionB)
require.NoError(t, err)
require.Equal(t, "203.0.113.42", got[model.SessionAttributesPropertyFieldIPAddress])
})
t.Run("Clear removes every entry", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionA := model.NewId()
sessionB := model.NewId()
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionA, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionB, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "203.0.113.42",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Clear())
_, _, err = cachedStore.SessionAttribute().Get(sessionA)
require.ErrorIs(t, err, cache.ErrKeyNotFound)
_, _, err = cachedStore.SessionAttribute().Get(sessionB)
require.ErrorIs(t, err, cache.ErrKeyNotFound)
})
t.Run("Refresh skips an update with an older timestamp", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionID := model.NewId()
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "203.0.113.42",
}, model.GetMillis()))
// A stale update (older timestamp) for the same key must not win.
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionID,
map[string]any{model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10"},
int64(1),
))
got, _, err := cachedStore.SessionAttribute().Get(sessionID)
require.NoError(t, err)
require.Equal(t, "203.0.113.42", got[model.SessionAttributesPropertyFieldIPAddress])
})
t.Run("cluster invalidation with clear-cache marker purges every entry", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
sessionA := model.NewId()
sessionB := model.NewId()
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionA, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
}, model.GetMillis()))
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionB, map[string]any{
model.SessionAttributesPropertyFieldIPAddress: "203.0.113.42",
}, model.GetMillis()))
cachedStore.sessionAttribute.handleClusterInvalidateSessionAttributes(&model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForSessionAttributes,
Data: clearCacheMessageData,
})
_, _, err = cachedStore.SessionAttribute().Get(sessionA)
require.ErrorIs(t, err, cache.ErrKeyNotFound)
_, _, err = cachedStore.SessionAttribute().Get(sessionB)
require.ErrorIs(t, err, cache.ErrKeyNotFound)
})
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheTeamStore struct {
store.TeamStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTeamStore) handleClusterInvalidateTeam(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.teamAllTeamIdsForUserCache.Purge()
} else {
s.rootStore.teamAllTeamIdsForUserCache.Remove(string(msg.Data))
}
}
func (s LocalCacheTeamStore) ClearCaches() {
s.rootStore.teamAllTeamIdsForUserCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.teamAllTeamIdsForUserCache.Name())
}
}
func (s LocalCacheTeamStore) InvalidateAllTeamIdsForUser(userId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.teamAllTeamIdsForUserCache, userId, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.teamAllTeamIdsForUserCache.Name())
}
}
func (s LocalCacheTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
if !allowFromCache {
return s.TeamStore.GetUserTeamIds(userID, allowFromCache)
}
var userTeamIds []string
if err := s.rootStore.doStandardReadCache(s.rootStore.teamAllTeamIdsForUserCache, userID, &userTeamIds); err == nil {
return userTeamIds, nil
}
userTeamIds, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err != nil {
return nil, err
}
if len(userTeamIds) > 0 {
s.rootStore.doStandardAddToCache(s.rootStore.teamAllTeamIdsForUserCache, userID, userTeamIds)
}
return userTeamIds, nil
}
func (s LocalCacheTeamStore) Update(team *model.Team) (*model.Team, error) {
var oldTeam *model.Team
var err error
if team.DeleteAt != 0 {
oldTeam, err = s.TeamStore.Get(team.Id)
if err != nil {
return nil, err
}
}
tm, err := s.TeamStore.Update(team)
if err != nil {
return nil, err
}
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
if oldTeam != nil && oldTeam.DeleteAt == 0 {
s.rootStore.doClearCacheCluster(s.rootStore.teamAllTeamIdsForUserCache)
}
return tm, err
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestTeamStore(t *testing.T) {
StoreTest(t, storetest.TestTeamStore)
}
func TestTeamStoreCache(t *testing.T) {
fakeUserId := "123"
fakeUserTeamIds := []string{"1", "2", "3"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUserTeamIds, err := cachedStore.Team().GetUserTeamIds(fakeUserId, true)
require.NoError(t, err)
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
gotUserTeamIds, err = cachedStore.Team().GetUserTeamIds(fakeUserId, true)
require.NoError(t, err)
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUserTeamIds, err := cachedStore.Team().GetUserTeamIds(fakeUserId, true)
require.NoError(t, err)
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
gotUserTeamIds, err = cachedStore.Team().GetUserTeamIds(fakeUserId, false)
require.NoError(t, err)
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 2)
})
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUserTeamIds, err := cachedStore.Team().GetUserTeamIds(fakeUserId, true)
require.NoError(t, err)
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
cachedStore.Team().InvalidateAllTeamIdsForUser(fakeUserId)
gotUserTeamIds, err = cachedStore.Team().GetUserTeamIds(fakeUserId, true)
require.NoError(t, err)
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 2)
})
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheTemporaryPostStore struct {
store.TemporaryPostStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTemporaryPostStore) handleClusterInvalidateTemporaryPosts(msg *model.ClusterMessage) {
if err := s.rootStore.temporaryPostCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge temporary post cache", mlog.Err(err))
}
}
func (s LocalCacheTemporaryPostStore) ClearCaches() {
if err := s.rootStore.temporaryPostCache.Purge(); err != nil {
s.rootStore.logger.Error("failed to purge temporary post cache", mlog.Err(err))
}
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.temporaryPostCache.Name())
}
}
func (s LocalCacheTemporaryPostStore) InvalidateTemporaryPost(id string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.temporaryPostCache, id, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.temporaryPostCache.Name())
}
}
func (s LocalCacheTemporaryPostStore) Save(rctx request.CTX, post *model.TemporaryPost) (*model.TemporaryPost, error) {
defer s.InvalidateTemporaryPost(post.ID)
return s.TemporaryPostStore.Save(rctx, post)
}
func (s LocalCacheTemporaryPostStore) Get(rctx request.CTX, id string, allowFromCache bool) (*model.TemporaryPost, error) {
if !allowFromCache {
return s.TemporaryPostStore.Get(rctx, id, false)
}
var post *model.TemporaryPost
if err := s.rootStore.doStandardReadCache(s.rootStore.temporaryPostCache, id, &post); err == nil {
return post, nil
}
post, err := s.TemporaryPostStore.Get(rctx, id, false)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.temporaryPostCache, id, post)
return post, nil
}
func (s LocalCacheTemporaryPostStore) Delete(rctx request.CTX, id string) error {
defer s.InvalidateTemporaryPost(id)
return s.TemporaryPostStore.Delete(rctx, id)
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestTemporaryPostStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestTemporaryPostStore)
}
func TestTemporaryPostStoreCache(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
temporaryPost, err := cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
assert.Equal(t, "123", temporaryPost.ID)
assert.Equal(t, "test message", temporaryPost.Message)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Second call should be cached
temporaryPost2, err := cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
assert.Equal(t, "123", temporaryPost2.ID)
assert.Equal(t, "test message", temporaryPost2.Message)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("first call not cached, second with allowFromCache=false bypasses cache", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First call - should cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Second call with allowFromCache=false - should bypass cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", false)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("allowFromCache=false always bypasses cache even when cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First call with caching enabled
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Second call with caching enabled - should use cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Third call with allowFromCache=false - should bypass cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", false)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 2)
// Fourth call with allowFromCache=false again - should bypass cache again
_, err = cachedStore.TemporaryPost().Get(nil, "123", false)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 3)
})
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First call - should cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Save should invalidate cache
tmpPost := &model.TemporaryPost{
ID: "123",
Type: model.PostTypeBurnOnRead,
ExpireAt: model.GetMillis() + 300000,
Message: "test message",
FileIDs: []string{"file1"},
}
_, err = cachedStore.TemporaryPost().Save(nil, tmpPost)
require.NoError(t, err)
// Next Get should hit the database again
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First call - should cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Delete should invalidate cache
err = cachedStore.TemporaryPost().Delete(nil, "123")
require.NoError(t, err)
// Next Get should hit the database again
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("cache invalidation via InvalidateTemporaryPost", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
// First call - should cache
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 1)
// Manual invalidation
cachedStore.TemporaryPost().InvalidateTemporaryPost("123")
// Next Get should hit the database again
_, err = cachedStore.TemporaryPost().Get(nil, "123", true)
require.NoError(t, err)
mockStore.TemporaryPost().(*mocks.TemporaryPostStore).AssertNumberOfCalls(t, "Get", 2)
})
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
const (
LatestKey = "latest"
)
type LocalCacheTermsOfServiceStore struct {
store.TermsOfServiceStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTermsOfServiceStore) handleClusterInvalidateTermsOfService(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.termsOfServiceCache.Purge()
} else {
s.rootStore.termsOfServiceCache.Remove(string(msg.Data))
}
}
func (s LocalCacheTermsOfServiceStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.termsOfServiceCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.termsOfServiceCache.Name())
}
}
func (s LocalCacheTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
tos, err := s.TermsOfServiceStore.Save(termsOfService)
if err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, tos.Id, tos)
s.rootStore.doInvalidateCacheCluster(s.rootStore.termsOfServiceCache, LatestKey, nil)
}
return tos, err
}
func (s LocalCacheTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
if allowFromCache {
var cacheItem *model.TermsOfService
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, LatestKey, &cacheItem); err == nil {
return cacheItem, nil
}
}
termsOfService, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, LatestKey, termsOfService)
}
return termsOfService, err
}
func (s LocalCacheTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
if allowFromCache {
var cacheItem *model.TermsOfService
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, id, &cacheItem); err == nil {
return cacheItem, nil
}
}
termsOfService, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
}
return termsOfService, err
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestTermsOfServiceStore(t *testing.T) {
StoreTest(t, storetest.TestTermsOfServiceStore)
}
func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) {
fakeTermsOfService := model.TermsOfService{Id: "123", CreateAt: 11111, UserId: "321", Text: "Terms of service test"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call by latest not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
termsOfService, err := cachedStore.TermsOfService().GetLatest(true)
require.NoError(t, err)
assert.Equal(t, termsOfService, &fakeTermsOfService)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
termsOfService, err = cachedStore.TermsOfService().GetLatest(true)
require.NoError(t, err)
assert.Equal(t, termsOfService, &fakeTermsOfService)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
})
t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
termsOfService, err := cachedStore.TermsOfService().Get("123", true)
require.NoError(t, err)
assert.Equal(t, termsOfService, &fakeTermsOfService)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
termsOfService, err = cachedStore.TermsOfService().Get("123", true)
require.NoError(t, err)
assert.Equal(t, termsOfService, &fakeTermsOfService)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("first call by id not cached, second force no cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().Get("123", true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.TermsOfService().Get("123", false)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call latest not cached, second force no cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().GetLatest(true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
cachedStore.TermsOfService().GetLatest(false)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
})
t.Run("first call by id force no cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().Get("123", false)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.TermsOfService().Get("123", true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
cachedStore.TermsOfService().Get("123", true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first call latest force no cached, second not cached, third cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().GetLatest(false)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
cachedStore.TermsOfService().GetLatest(true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
cachedStore.TermsOfService().GetLatest(true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
})
t.Run("first call latest, second call by id cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().GetLatest(true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
cachedStore.TermsOfService().Get("123", true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 0)
})
t.Run("first call by id not cached, save, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().Get("123", false)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.TermsOfService().Save(&fakeTermsOfService)
cachedStore.TermsOfService().Get("123", false)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("first get latest not cached, save new, then get latest, returning different data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.TermsOfService().GetLatest(true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
cachedStore.TermsOfService().Save(&fakeTermsOfService)
cachedStore.TermsOfService().GetLatest(true)
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
})
}

View File

@@ -0,0 +1,368 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"slices"
"sync"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
)
type LocalCacheUserStore struct {
store.UserStore
rootStore *LocalCacheStore
userProfileByIdsMut sync.Mutex
userProfileByIdsInvalidations map[string]bool
}
const allUserKey = "ALL"
func (s *LocalCacheUserStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.userProfileByIdsCache.Purge()
} else {
s.userProfileByIdsMut.Lock()
s.userProfileByIdsInvalidations[string(msg.Data)] = true
s.userProfileByIdsMut.Unlock()
s.rootStore.userProfileByIdsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) handleClusterInvalidateProfilesInChannel(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.profilesInChannelCache.Purge()
} else {
s.rootStore.profilesInChannelCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) handleClusterInvalidateAllProfiles(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.allUserCache.Purge()
} else {
s.rootStore.allUserCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) ClearCaches() {
s.rootStore.userProfileByIdsCache.Purge()
s.rootStore.allUserCache.Purge()
s.rootStore.profilesInChannelCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userProfileByIdsCache.Name())
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.profilesInChannelCache.Name())
}
}
func (s *LocalCacheUserStore) InvalidateProfileCacheForUser(userId string) {
s.userProfileByIdsMut.Lock()
s.userProfileByIdsInvalidations[userId] = true
s.userProfileByIdsMut.Unlock()
s.rootStore.doInvalidateCacheCluster(s.rootStore.userProfileByIdsCache, userId, nil)
s.rootStore.doInvalidateCacheCluster(s.rootStore.allUserCache, allUserKey, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.userProfileByIdsCache.Name())
}
}
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {
var toDelete []string
err := s.rootStore.profilesInChannelCache.Scan(func(keys []string) error {
if len(keys) == 0 {
return nil
}
toPass := allocateCacheTargets[model.UserMap](len(keys))
errs := s.rootStore.doMultiReadCache(s.rootStore.profilesInChannelCache, keys, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
return err
}
continue
}
gotMap := *(toPass[i].(*model.UserMap))
if gotMap == nil {
s.rootStore.logger.Warn("Found nil userMap in InvalidateProfilesInChannelCacheByUser. This is not expected")
continue
}
if _, ok := gotMap[userId]; ok {
toDelete = append(toDelete, keys[i])
}
}
return nil
})
if err != nil {
s.rootStore.logger.Warn("Error while scanning in InvalidateProfilesInChannelCacheByUser", mlog.Err(err))
return
}
s.rootStore.doMultiInvalidateCacheCluster(s.rootStore.profilesInChannelCache, toDelete, nil)
}
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCache(channelID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, channelID, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.profilesInChannelCache.Name())
}
}
func (s *LocalCacheUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
if isEmptyOptions(options) &&
options.Page == 0 && options.PerPage == 100 { // This is hardcoded to the webapp call.
// read from cache
var users []*model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.allUserCache, allUserKey, &users); err == nil {
return users, nil
}
users, err := s.UserStore.GetAllProfiles(options)
if err != nil {
return nil, err
}
// populate the cache only for those options.
s.rootStore.doStandardAddToCache(s.rootStore.allUserCache, allUserKey, users)
return users, nil
}
// For any other case, simply use the store
return s.UserStore.GetAllProfiles(options)
}
func (s *LocalCacheUserStore) GetAllProfilesInChannel(rctx request.CTX, channelId string, allowFromCache bool) (map[string]*model.User, error) {
if allowFromCache {
var cachedMap model.UserMap
if err := s.rootStore.doStandardReadCache(s.rootStore.profilesInChannelCache, channelId, &cachedMap); err == nil {
return cachedMap, nil
}
}
userMap, err := s.UserStore.GetAllProfilesInChannel(rctx, channelId, allowFromCache)
if err != nil {
return nil, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.profilesInChannelCache, channelId, model.UserMap(userMap))
}
return userMap, nil
}
func (s *LocalCacheUserStore) GetProfileByIds(rctx request.CTX, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
if !allowFromCache {
return s.UserStore.GetProfileByIds(rctx, userIds, options, false)
}
if options == nil {
options = &store.UserGetByIdsOpts{}
}
users := []*model.User{}
remainingUserIds := make([]string, 0)
fromMaster := false
toPass := allocateCacheTargets[model.User](len(userIds))
errs := s.rootStore.doMultiReadCache(s.rootStore.userProfileByIdsCache, userIds, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
s.rootStore.logger.Warn("Error in UserStore.GetProfileByIds: ", mlog.Err(err))
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[userIds[i]] {
fromMaster = true
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, userIds[i])
}
s.userProfileByIdsMut.Unlock()
remainingUserIds = append(remainingUserIds, userIds[i])
} else {
gotUser := toPass[i].(*model.User)
if (gotUser != nil) && (options.Since == 0 || gotUser.UpdateAt > options.Since) {
users = append(users, gotUser)
} else if gotUser == nil {
s.rootStore.logger.Warn("Found nil user in GetProfileByIds. This is not expected")
}
}
}
if len(remainingUserIds) > 0 {
if fromMaster {
rctx = sqlstore.RequestContextWithMaster(rctx)
}
remainingUsers, err := s.UserStore.GetProfileByIds(rctx, remainingUserIds, options, false)
if err != nil {
return nil, err
}
for _, user := range remainingUsers {
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
users = append(users, user)
}
}
return users, nil
}
func (s *LocalCacheUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
s.InvalidateProfileCacheForUser(userID)
return s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
}
func (s *LocalCacheUserStore) TryIncrementFailedPasswordAttempts(userID string, maxAttempts int) (bool, error) {
claimed, err := s.UserStore.TryIncrementFailedPasswordAttempts(userID, maxAttempts)
if err != nil {
return false, err
}
if claimed {
s.InvalidateProfileCacheForUser(userID)
}
return claimed, nil
}
func (s *LocalCacheUserStore) DecrementFailedPasswordAttempts(userID string) error {
if err := s.UserStore.DecrementFailedPasswordAttempts(userID); err != nil {
return err
}
s.InvalidateProfileCacheForUser(userID)
return nil
}
// Get is a cache wrapper around the SqlStore method to get a user profile by id.
// It checks if the user entry is present in the cache, returning the entry from cache
// if it is present. Otherwise, it fetches the entry from the store and stores it in the
// cache.
func (s *LocalCacheUserStore) Get(rctx request.CTX, id string) (*model.User, error) {
var cacheItem model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cacheItem); err == nil {
return &cacheItem, nil
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[id] {
rctx = sqlstore.RequestContextWithMaster(rctx)
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, id)
}
s.userProfileByIdsMut.Unlock()
user, err := s.UserStore.Get(rctx, id)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, id, user)
return user, nil
}
// GetMany is a cache wrapper around the SqlStore method to get a user profiles by ids.
// It checks if the user entries are present in the cache, returning the entries from cache
// if it is present. Otherwise, it fetches the entries from the store and stores it in the
// cache.
func (s *LocalCacheUserStore) GetMany(rctx request.CTX, ids []string) ([]*model.User, error) {
// we are doing a loop instead of caching the full set in the cache because the number of permutations that we can have
// in this func is making caching of the total set not beneficial.
var cachedUsers []*model.User
var notCachedUserIds []string
uniqIDs := dedup(ids)
fromMaster := false
toPass := allocateCacheTargets[model.User](len(uniqIDs))
errs := s.rootStore.doMultiReadCache(s.rootStore.userProfileByIdsCache, uniqIDs, toPass)
for i, err := range errs {
if err != nil {
if err != cache.ErrKeyNotFound {
s.rootStore.logger.Warn("Error in UserStore.GetMany: ", mlog.Err(err))
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[uniqIDs[i]] {
fromMaster = true
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, uniqIDs[i])
}
s.userProfileByIdsMut.Unlock()
notCachedUserIds = append(notCachedUserIds, uniqIDs[i])
} else {
gotUser := toPass[i].(*model.User)
if gotUser != nil {
cachedUsers = append(cachedUsers, gotUser)
} else {
s.rootStore.logger.Warn("Found nil user in GetMany. This is not expected")
}
}
}
if len(notCachedUserIds) > 0 {
if fromMaster {
rctx = sqlstore.RequestContextWithMaster(rctx)
}
dbUsers, err := s.UserStore.GetMany(rctx, notCachedUserIds)
if err != nil {
return nil, err
}
for _, user := range dbUsers {
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
cachedUsers = append(cachedUsers, user)
}
}
return cachedUsers, nil
}
func dedup(elements []string) []string {
if len(elements) == 0 {
return elements
}
slices.Sort(elements)
j := 0
for i := 1; i < len(elements); i++ {
if elements[j] == elements[i] {
continue
}
j++
// preserve the original data
// in[i], in[j] = in[j], in[i]
// only set what is required
elements[j] = elements[i]
}
return elements[:j+1]
}
func isEmptyOptions(options *model.UserGetOptions) bool {
// We check to see if any of the options are set or not, and then
// use the cache only if none are set, which is the most common case.
// options.WithoutTeam, Sort is unused
if options.InTeamId == "" &&
options.NotInTeamId == "" &&
options.InChannelId == "" &&
options.NotInChannelId == "" &&
options.InGroupId == "" &&
options.NotInGroupId == "" &&
!options.GroupConstrained &&
!options.Inactive &&
!options.Active &&
options.Role == "" &&
len(options.Roles) == 0 &&
len(options.ChannelRoles) == 0 &&
len(options.TeamRoles) == 0 &&
options.ViewRestrictions == nil {
return true
}
return false
}

View File

@@ -0,0 +1,400 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
cmocks "github.com/mattermost/mattermost/server/v8/platform/services/cache/mocks"
)
func TestUserStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestUserStore)
}
func TestUserStoreCache(t *testing.T) {
rctx := request.TestContext(t)
fakeUserIds := []string{"123"}
fakeUser := []*model.User{{
Id: "123",
AuthData: new("authData"),
AuthService: "authService",
}}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUser, err := cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
require.NoError(t, err)
assert.Equal(t, fakeUser, gotUser)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 1)
_, _ = cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUser, err := cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
require.NoError(t, err)
assert.Equal(t, fakeUser, gotUser)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 1)
_, _ = cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, false)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 2)
})
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUser, err := cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
require.NoError(t, err)
assert.Equal(t, fakeUser, gotUser)
cachedStore.User().InvalidateProfileCacheForUser("123")
_, _ = cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 2)
})
t.Run("should always return a copy of the stored data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
storedUsers, err := mockStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, false)
require.NoError(t, err)
originalProps := make([]model.StringMap, len(storedUsers))
for i := range storedUsers {
originalProps[i] = storedUsers[i].NotifyProps
storedUsers[i].NotifyProps = map[string]string{}
storedUsers[i].NotifyProps["key"] = "somevalue"
}
cachedUsers, err := cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
require.NoError(t, err)
for i := range storedUsers {
assert.Equal(t, storedUsers[i].Id, cachedUsers[i].Id)
}
cachedUsers, err = cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
require.NoError(t, err)
for i := range storedUsers {
storedUsers[i].Props = model.StringMap{}
storedUsers[i].Timezone = model.StringMap{}
assert.Equal(t, storedUsers[i], cachedUsers[i])
if storedUsers[i] == cachedUsers[i] {
assert.Fail(t, "should be different pointers")
}
cachedUsers[i].NotifyProps["key"] = "othervalue"
assert.NotEqual(t, storedUsers[i], cachedUsers[i])
}
for i := range storedUsers {
storedUsers[i].NotifyProps = originalProps[i]
}
})
t.Run("assert **model.User not passed", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cmock := cmocks.NewCache(t)
cmock.On("GetMulti", []string{"123"}, mock.MatchedBy(func(values []any) bool {
if len(values) != 1 {
return false
}
_, ok := values[0].(*model.User)
return ok
})).Return(nil)
cachedStore.user.rootStore.userProfileByIdsCache = cmock
_, err = cachedStore.User().GetProfileByIds(rctx, fakeUserIds, &store.UserGetByIdsOpts{}, true)
require.NoError(t, err)
})
}
func TestUserStoreGetAllProfiles(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
users, err := cachedStore.User().GetAllProfiles(&model.UserGetOptions{Page: 0, PerPage: 100})
require.NoError(t, err)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfiles", 1)
users2, _ := cachedStore.User().GetAllProfiles(&model.UserGetOptions{Page: 0, PerPage: 100})
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfiles", 1)
assert.Equal(t, users, users2)
_, _ = cachedStore.User().GetAllProfiles(&model.UserGetOptions{Page: 2, PerPage: 1})
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfiles", 2)
assert.Equal(t, users, users2)
})
t.Run("different page sizes aren't cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, _ = cachedStore.User().GetAllProfiles(&model.UserGetOptions{Page: 0, PerPage: 100})
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfiles", 1)
_, _ = cachedStore.User().GetAllProfiles(&model.UserGetOptions{Page: 2, PerPage: 1})
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfiles", 2)
_, _ = cachedStore.User().GetAllProfiles(&model.UserGetOptions{Page: 1, PerPage: 2})
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfiles", 3)
})
}
func TestUserStoreProfilesInChannelCache(t *testing.T) {
rctx := request.TestContext(t)
fakeChannelId := "123"
fakeUserId := "456"
fakeMap := map[string]*model.User{
fakeUserId: {Id: "456"},
}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotMap, err := cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
require.NoError(t, err)
assert.Equal(t, fakeMap, gotMap)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
_, _ = cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotMap, err := cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
require.NoError(t, err)
assert.Equal(t, fakeMap, gotMap)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
_, _ = cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, false)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 2)
})
t.Run("first call not cached, invalidate by channel, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotMap, err := cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
require.NoError(t, err)
assert.Equal(t, fakeMap, gotMap)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
cachedStore.User().InvalidateProfilesInChannelCache("123")
_, _ = cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 2)
})
t.Run("first call not cached, invalidate by user, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotMap, err := cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
require.NoError(t, err)
assert.Equal(t, fakeMap, gotMap)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
cachedStore.User().InvalidateProfilesInChannelCacheByUser("456")
_, _ = cachedStore.User().GetAllProfilesInChannel(rctx, fakeChannelId, true)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 2)
})
}
func TestUserStoreGetCache(t *testing.T) {
rctx := request.TestContext(t)
fakeUserId := "123"
fakeUser := &model.User{
Id: "123",
AuthData: new("authData"),
AuthService: "authService",
}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUser, err := cachedStore.User().Get(rctx, fakeUserId)
require.NoError(t, err)
assert.Equal(t, fakeUser, gotUser)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 1)
_, _ = cachedStore.User().Get(rctx, fakeUserId)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 1)
})
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUser, err := cachedStore.User().Get(rctx, fakeUserId)
require.NoError(t, err)
assert.Equal(t, fakeUser, gotUser)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 1)
cachedStore.User().InvalidateProfileCacheForUser("123")
_, _ = cachedStore.User().Get(rctx, fakeUserId)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 2)
})
t.Run("should always return a copy of the stored data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
storedUser, err := mockStore.User().Get(rctx, fakeUserId)
require.NoError(t, err)
originalProps := storedUser.NotifyProps
storedUser.NotifyProps = map[string]string{}
storedUser.NotifyProps["key"] = "somevalue"
cachedUser, err := cachedStore.User().Get(rctx, fakeUserId)
require.NoError(t, err)
assert.Equal(t, storedUser, cachedUser)
storedUser.Props = model.StringMap{}
storedUser.Timezone = model.StringMap{}
cachedUser, err = cachedStore.User().Get(rctx, fakeUserId)
require.NoError(t, err)
assert.Equal(t, storedUser, cachedUser)
if storedUser == cachedUser {
assert.Fail(t, "should be different pointers")
}
cachedUser.NotifyProps["key"] = "othervalue"
assert.NotEqual(t, storedUser, cachedUser)
storedUser.NotifyProps = originalProps
})
t.Run("assert **model.User not passed", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cmock := cmocks.NewCache(t)
cmock.On("Get", "123", mock.AnythingOfType("*model.User")).Return(nil)
cachedStore.user.rootStore.userProfileByIdsCache = cmock
_, err = cachedStore.User().Get(rctx, fakeUserId)
require.NoError(t, err)
})
}
func TestUserStoreGetManyCache(t *testing.T) {
rctx := request.TestContext(t)
fakeUser := &model.User{
Id: "123",
AuthData: new("authData"),
AuthService: "authService",
}
otherFakeUser := &model.User{
Id: "456",
AuthData: new("authData"),
AuthService: "authService",
}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUsers, err := cachedStore.User().GetMany(rctx, []string{fakeUser.Id, otherFakeUser.Id})
require.NoError(t, err)
assert.Len(t, gotUsers, 2)
assert.Contains(t, gotUsers, fakeUser)
assert.Contains(t, gotUsers, otherFakeUser)
gotUsers, err = cachedStore.User().GetMany(rctx, []string{fakeUser.Id, otherFakeUser.Id})
require.NoError(t, err)
assert.Len(t, gotUsers, 2)
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetMany", 1)
})
t.Run("first call not cached, invalidate one user, and then check that one is cached and one is fetched from db", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
gotUsers, err := cachedStore.User().GetMany(rctx, []string{fakeUser.Id, otherFakeUser.Id})
require.NoError(t, err)
assert.Len(t, gotUsers, 2)
assert.Contains(t, gotUsers, fakeUser)
assert.Contains(t, gotUsers, otherFakeUser)
cachedStore.User().InvalidateProfileCacheForUser("123")
gotUsers, err = cachedStore.User().GetMany(rctx, []string{fakeUser.Id, otherFakeUser.Id})
require.NoError(t, err)
assert.Len(t, gotUsers, 2)
mockStore.User().(*mocks.UserStore).AssertCalled(t, "GetMany", mock.Anything, []string{"123"})
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetMany", 2)
})
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type LocalCacheWebhookStore struct {
store.WebhookStore
rootStore *LocalCacheStore
}
func (s *LocalCacheWebhookStore) handleClusterInvalidateWebhook(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.webhookCache.Purge()
} else {
s.rootStore.webhookCache.Remove(string(msg.Data))
}
}
func (s LocalCacheWebhookStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.webhookCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.webhookCache.Name())
}
}
func (s LocalCacheWebhookStore) InvalidateWebhookCache(webhookId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.webhookCache, webhookId, nil)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter(s.rootStore.webhookCache.Name())
}
}
func (s LocalCacheWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
if !allowFromCache {
return s.WebhookStore.GetIncoming(id, allowFromCache)
}
var incomingWebhook *model.IncomingWebhook
if err := s.rootStore.doStandardReadCache(s.rootStore.webhookCache, id, &incomingWebhook); err == nil {
return incomingWebhook, nil
}
incomingWebhook, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.webhookCache, id, incomingWebhook)
return incomingWebhook, nil
}
func (s LocalCacheWebhookStore) DeleteIncoming(webhookId string, time int64) error {
err := s.WebhookStore.DeleteIncoming(webhookId, time)
if err != nil {
return err
}
s.InvalidateWebhookCache(webhookId)
return nil
}
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByUser(userId string) error {
err := s.WebhookStore.PermanentDeleteIncomingByUser(userId)
if err != nil {
return err
}
s.ClearCaches()
return nil
}
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByChannel(channelId string) error {
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelId)
if err != nil {
return err
}
s.ClearCaches()
return nil
}
func (s LocalCacheWebhookStore) UpdateIncomingLastUsed(webhookID string, lastUsed int64) error {
err := s.WebhookStore.UpdateIncomingLastUsed(webhookID, lastUsed)
if err != nil {
return err
}
s.InvalidateWebhookCache(webhookID)
return nil
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestWebhookStore(t *testing.T) {
StoreTest(t, storetest.TestWebhookStore)
}
func TestWebhookStoreCache(t *testing.T) {
fakeWebhook := model.IncomingWebhook{Id: "123"}
logger := mlog.CreateConsoleTestLogger(t)
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
incomingWebhook, err := cachedStore.Webhook().GetIncoming("123", true)
require.NoError(t, err)
assert.Equal(t, incomingWebhook, &fakeWebhook)
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
assert.Equal(t, incomingWebhook, &fakeWebhook)
cachedStore.Webhook().GetIncoming("123", true)
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
})
t.Run("first call not cached, second force not cached", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Webhook().GetIncoming("123", true)
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
cachedStore.Webhook().GetIncoming("123", false)
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 2)
})
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Webhook().GetIncoming("123", true)
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
cachedStore.Webhook().InvalidateWebhookCache("123")
cachedStore.Webhook().GetIncoming("123", true)
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 2)
})
}