MM42267: Add member count in the browse channel modal (#23800)

* add base for calling the endpoint

* add endpoint and handler

* update store and layers

* call the endpoint

* align types

* update app layers

* generate mocks

* complete handler

* finish store query

* add todos

* add ui for member count

* add selector

* add a todo

* add cache layer

* optimize calls in FE

* handle invalidation of the cache

* fix go style

* fix test

* use existing channel layer count

* fix import error

* delete unnecessary code

* write tests for channel cache layer

* fix testname

* fix mocks

* fix cache layer test

* fix a test

* really fix the test

* write more tests for server

* address PR comments

* remove comment

* rename more_channels to browse_channels

* fix style

* update snapshot

* add translations

* Revert "add translations"

This reverts commit 56476a5dabe357703ef02be9a38b3e88f5c8b1e7.

* add only related translations

* address PR review points

* add test

* fix test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Sinan Sonmez (Chaush)
2023-07-19 08:15:27 +02:00
коммит произвёл GitHub
родитель 4803889158
Коммит 628273d98d
37 изменённых файлов: 591 добавлений и 87 удалений

Просмотреть файл

@@ -225,6 +225,35 @@ func (s LocalCacheChannelStore) SaveMultipleMembers(members []*model.ChannelMemb
return members, nil
}
func (s LocalCacheChannelStore) GetChannelsMemberCount(channelIDs []string) (_ map[string]int64, err error) {
counts := make(map[string]int64)
remainingChannels := make([]string, 0)
for _, channelID := range channelIDs {
var cacheItem int64
err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelID, &cacheItem)
if err == nil {
counts[channelID] = cacheItem
} else {
remainingChannels = append(remainingChannels, channelID)
}
}
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(member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.UpdateMember(member)
if err != nil {

Просмотреть файл

@@ -104,6 +104,43 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) {
})
}
func TestChannelStoreChannelsMemberCountCache(t *testing.T) {
channelsCountResult := map[string]int64{
"channel1": 10,
"channel2": 20,
}
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
mockStore := getMockStore()
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
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()
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
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)

Просмотреть файл

@@ -100,6 +100,12 @@ func getMockStore() *mocks.Store {
mockChannelStore.On("Get", channelId, false).Return(&fakeChannelId, 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)

Просмотреть файл

@@ -1269,6 +1269,24 @@ func (s *OpenTracingLayerChannelStore) GetChannelsByUser(userID string, includeD
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsMemberCount(channelIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithCursor")

Просмотреть файл

@@ -1406,6 +1406,27 @@ func (s *RetryLayerChannelStore) GetChannelsByUser(userID string, includeDeleted
}
func (s *RetryLayerChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsMemberCount(channelIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
tries := 0

Просмотреть файл

@@ -2207,6 +2207,47 @@ func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCac
return ids, nil
}
func (s SqlChannelStore) GetChannelsMemberCount(channelIDs []string) (_ map[string]int64, err error) {
query := s.getQueryBuilder().
Select("ChannelMembers.ChannelId,COUNT(*) AS Count").
From("ChannelMembers").
InnerJoin("Users ON ChannelMembers.UserId = Users.Id").
Where(sq.And{
sq.Eq{"ChannelMembers.ChannelId": channelIDs},
sq.Eq{"Users.DeleteAt": 0},
}).
GroupBy("ChannelMembers.ChannelId")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channels_member_count_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch member counts")
}
defer rows.Close()
memberCounts := make(map[string]int64)
for rows.Next() {
var channelID string
var count int64
errScan := rows.Scan(&channelID, &count)
if errScan != nil {
return nil, errors.Wrap(err, "failed to scan row")
}
memberCounts[channelID] = count
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "error while iterating rows")
}
return memberCounts, nil
}
func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
if s.metrics != nil {

Просмотреть файл

@@ -222,6 +222,7 @@ type ChannelStore interface {
GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error)
GetChannelMembersTimezones(channelID string) ([]model.StringMap, error)
GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error)
GetChannelsMemberCount(channelIDs []string) (map[string]int64, error)
InvalidateAllChannelMembersForUser(userID string)
IsUserInChannelUseCache(userID string, channelID string) bool
GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error)

Просмотреть файл

@@ -910,6 +910,32 @@ func (_m *ChannelStore) GetChannelsByUser(userID string, includeDeleted bool, la
return r0, r1
}
// GetChannelsMemberCount provides a mock function with given fields: channelIDs
func (_m *ChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
ret := _m.Called(channelIDs)
var r0 map[string]int64
var r1 error
if rf, ok := ret.Get(0).(func([]string) (map[string]int64, error)); ok {
return rf(channelIDs)
}
if rf, ok := ret.Get(0).(func([]string) map[string]int64); ok {
r0 = rf(channelIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]int64)
}
}
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(channelIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsWithCursor provides a mock function with given fields: teamId, userId, opts, afterChannelID
func (_m *ChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
ret := _m.Called(teamId, userId, opts, afterChannelID)

Просмотреть файл

@@ -1184,6 +1184,22 @@ func (s *TimerLayerChannelStore) GetChannelsByUser(userID string, includeDeleted
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsMemberCount(channelIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
start := time.Now()