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
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:
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
59
server/channels/store/localcachelayer/attributes_layer.go
Normal file
59
server/channels/store/localcachelayer/attributes_layer.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
195
server/channels/store/localcachelayer/autotranslation_layer.go
Normal file
195
server/channels/store/localcachelayer/autotranslation_layer.go
Normal 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
|
||||
}
|
||||
568
server/channels/store/localcachelayer/channel_layer.go
Normal file
568
server/channels/store/localcachelayer/channel_layer.go
Normal 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
|
||||
}
|
||||
538
server/channels/store/localcachelayer/channel_layer_test.go
Normal file
538
server/channels/store/localcachelayer/channel_layer_test.go
Normal 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)
|
||||
}
|
||||
54
server/channels/store/localcachelayer/content_flagging.go
Normal file
54
server/channels/store/localcachelayer/content_flagging.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
136
server/channels/store/localcachelayer/delivery_tracking.go
Normal file
136
server/channels/store/localcachelayer/delivery_tracking.go
Normal 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()
|
||||
}
|
||||
198
server/channels/store/localcachelayer/delivery_tracking_test.go
Normal file
198
server/channels/store/localcachelayer/delivery_tracking_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
222
server/channels/store/localcachelayer/emoji_layer.go
Normal file
222
server/channels/store/localcachelayer/emoji_layer.go
Normal 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)
|
||||
}
|
||||
332
server/channels/store/localcachelayer/emoji_layer_test.go
Normal file
332
server/channels/store/localcachelayer/emoji_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
139
server/channels/store/localcachelayer/file_info_layer.go
Normal file
139
server/channels/store/localcachelayer/file_info_layer.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
836
server/channels/store/localcachelayer/layer.go
Normal file
836
server/channels/store/localcachelayer/layer.go
Normal 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
|
||||
}
|
||||
167
server/channels/store/localcachelayer/layer_test.go
Normal file
167
server/channels/store/localcachelayer/layer_test.go
Normal 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)
|
||||
}
|
||||
269
server/channels/store/localcachelayer/main_test.go
Normal file
269
server/channels/store/localcachelayer/main_test.go
Normal 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()
|
||||
}
|
||||
179
server/channels/store/localcachelayer/post_layer.go
Normal file
179
server/channels/store/localcachelayer/post_layer.go
Normal 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
|
||||
}
|
||||
256
server/channels/store/localcachelayer/post_layer_test.go
Normal file
256
server/channels/store/localcachelayer/post_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
62
server/channels/store/localcachelayer/reaction_layer.go
Normal file
62
server/channels/store/localcachelayer/reaction_layer.go
Normal 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)
|
||||
}
|
||||
78
server/channels/store/localcachelayer/reaction_layer_test.go
Normal file
78
server/channels/store/localcachelayer/reaction_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
188
server/channels/store/localcachelayer/read_receipt_layer.go
Normal file
188
server/channels/store/localcachelayer/read_receipt_layer.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
140
server/channels/store/localcachelayer/role_layer.go
Normal file
140
server/channels/store/localcachelayer/role_layer.go
Normal 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
|
||||
}
|
||||
131
server/channels/store/localcachelayer/role_layer_test.go
Normal file
131
server/channels/store/localcachelayer/role_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
63
server/channels/store/localcachelayer/scheme_layer.go
Normal file
63
server/channels/store/localcachelayer/scheme_layer.go
Normal 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()
|
||||
}
|
||||
80
server/channels/store/localcachelayer/scheme_layer_test.go
Normal file
80
server/channels/store/localcachelayer/scheme_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
83
server/channels/store/localcachelayer/team_layer.go
Normal file
83
server/channels/store/localcachelayer/team_layer.go
Normal 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
|
||||
}
|
||||
78
server/channels/store/localcachelayer/team_layer_test.go
Normal file
78
server/channels/store/localcachelayer/team_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
368
server/channels/store/localcachelayer/user_layer.go
Normal file
368
server/channels/store/localcachelayer/user_layer.go
Normal 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
|
||||
}
|
||||
400
server/channels/store/localcachelayer/user_layer_test.go
Normal file
400
server/channels/store/localcachelayer/user_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
99
server/channels/store/localcachelayer/webhook_layer.go
Normal file
99
server/channels/store/localcachelayer/webhook_layer.go
Normal 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
|
||||
}
|
||||
66
server/channels/store/localcachelayer/webhook_layer_test.go
Normal file
66
server/channels/store/localcachelayer/webhook_layer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user