From b9a3c2e90ed6d59c04bdd982833b5cba2eff9643 Mon Sep 17 00:00:00 2001 From: Woolim Cho Date: Wed, 4 Sep 2019 20:41:29 +0900 Subject: [PATCH] MM-10272: Add a count for pinned posts header icon (#11840) --- api4/channel.go | 8 ++- api4/channel_test.go | 10 ++++ app/channel.go | 4 ++ app/web_hub.go | 1 + i18n/en.json | 4 ++ model/channel_stats.go | 7 +-- store/sqlstore/channel_store.go | 66 ++++++++++++++++++++++++ store/store.go | 3 ++ store/storetest/channel_store.go | 73 +++++++++++++++++++++++++++ store/storetest/mocks/ChannelStore.go | 42 +++++++++++++++ store/timer_layer.go | 17 +++++++ 11 files changed, 231 insertions(+), 4 deletions(-) diff --git a/api4/channel.go b/api4/channel.go index bc7bb9c0b5..03edc36a7b 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -527,7 +527,13 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { return } - stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount, GuestCount: guestCount} + pinnedPostCount, err := c.App.GetChannelPinnedPostCount(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount, GuestCount: guestCount, PinnedPostCount: pinnedPostCount} w.Write([]byte(stats.ToJson())) } diff --git a/api4/channel_test.go b/api4/channel_test.go index 8473f8c587..fb7ac11d15 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1849,6 +1849,16 @@ func TestGetChannelStats(t *testing.T) { t.Fatal("couldnt't get extra info") } else if stats.MemberCount != 1 { t.Fatal("got incorrect member count") + } else if stats.PinnedPostCount != 0 { + t.Fatal("got incorrect pinned post count") + } + + th.CreatePinnedPostWithClient(th.Client, channel) + stats, resp = Client.GetChannelStats(channel.Id, "") + CheckNoError(t, resp) + + if stats.PinnedPostCount != 1 { + t.Fatal("should have returned 1 pinned post count") } _, resp = Client.GetChannelStats("junk", "") diff --git a/app/channel.go b/app/channel.go index 7fad7095db..c9388da7b1 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1330,6 +1330,10 @@ func (a *App) GetChannelGuestCount(channelId string) (int64, *model.AppError) { return a.Srv.Store.Channel().GetGuestCount(channelId, true) } +func (a *App) GetChannelPinnedPostCount(channelId string) (int64, *model.AppError) { + return a.Srv.Store.Channel().GetPinnedPostCount(channelId, true) +} + func (a *App) GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *model.AppError) { return a.Srv.Store.Channel().GetChannelCounts(teamId, userId) } diff --git a/app/web_hub.go b/app/web_hub.go index babf49851b..8c5e828c1e 100644 --- a/app/web_hub.go +++ b/app/web_hub.go @@ -289,6 +289,7 @@ func (a *App) InvalidateCacheForChannelPosts(channelId string) { func (a *App) InvalidateCacheForChannelPostsSkipClusterSend(channelId string) { a.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + a.Srv.Store.Channel().InvalidatePinnedPostCount(channelId) } func (a *App) InvalidateCacheForUser(userId string) { diff --git a/i18n/en.json b/i18n/en.json index af1be7e046..71256f8f08 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5658,6 +5658,10 @@ "id": "store.sql_channel.get_more_channels.get.app_error", "translation": "Unable to get the channels" }, + { + "id": "store.sql_channel.get_pinnedpost_count.app_error", + "translation": "Unable to get the channel pinned post count" + }, { "id": "store.sql_channel.get_public_channels.get.app_error", "translation": "Unable to get public channels" diff --git a/model/channel_stats.go b/model/channel_stats.go index 5eb26b7e29..63367fa48f 100644 --- a/model/channel_stats.go +++ b/model/channel_stats.go @@ -9,9 +9,10 @@ import ( ) type ChannelStats struct { - ChannelId string `json:"channel_id"` - MemberCount int64 `json:"member_count"` - GuestCount int64 `json:"guest_count"` + ChannelId string `json:"channel_id"` + MemberCount int64 `json:"member_count"` + GuestCount int64 `json:"guest_count"` + PinnedPostCount int64 `json:"pinnedpost_count"` } func (o *ChannelStats) ToJson() string { diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 5ff8af8513..ae01300781 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -35,6 +35,9 @@ const ( CHANNEL_GUESTS_COUNTS_CACHE_SIZE = model.CHANNEL_CACHE_SIZE CHANNEL_GUESTS_COUNTS_CACHE_SEC = 1800 // 30 mins + CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SIZE = model.CHANNEL_CACHE_SIZE + CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SEC = 1800 // 30 mins + CHANNEL_CACHE_SEC = 900 // 15 mins ) @@ -281,6 +284,7 @@ type publicChannel struct { } var channelMemberCountsCache = utils.NewLru(CHANNEL_MEMBERS_COUNTS_CACHE_SIZE) +var channelPinnedPostCountsCache = utils.NewLru(CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SIZE) var channelGuestCountsCache = utils.NewLru(CHANNEL_GUESTS_COUNTS_CACHE_SIZE) var allChannelMembersForUserCache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE) var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE) @@ -289,6 +293,7 @@ var channelByNameCache = utils.NewLru(model.CHANNEL_CACHE_SIZE) func (s SqlChannelStore) ClearCaches() { channelMemberCountsCache.Purge() + channelPinnedPostCountsCache.Purge() channelGuestCountsCache.Purge() allChannelMembersForUserCache.Purge() allChannelMembersNotifyPropsForChannelCache.Purge() @@ -297,6 +302,7 @@ func (s SqlChannelStore) ClearCaches() { if s.metrics != nil { s.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Purge") s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge") s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge") s.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge") @@ -1626,6 +1632,66 @@ func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) ( return count, nil } +func (s SqlChannelStore) InvalidatePinnedPostCount(channelId string) { + channelPinnedPostCountsCache.Remove(channelId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Remove by ChannelId") + } +} + +func (s SqlChannelStore) GetPinnedPostCountFromCache(channelId string) int64 { + if cacheItem, ok := channelPinnedPostCountsCache.Get(channelId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("Channel Pinned Post Counts") + } + return cacheItem.(int64) + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("Channel Pinned Post Counts") + } + + count, err := s.GetPinnedPostCount(channelId, true) + if err != nil { + return 0 + } + + return count +} + +func (s SqlChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) { + if allowFromCache { + if cacheItem, ok := channelPinnedPostCountsCache.Get(channelId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("Channel Pinned Post Counts") + } + return cacheItem.(int64), nil + } + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("Channel Pinned Post Counts") + } + + count, err := s.GetReplica().SelectInt(` + SELECT count(*) + FROM Posts + WHERE + IsPinned = true + AND ChannelId = :ChannelId + AND DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId}) + + if err != nil { + return 0, model.NewAppError("SqlChannelStore.GetPinnedPostCount", "store.sql_channel.get_pinnedpost_count.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) + } + + if allowFromCache { + channelPinnedPostCountsCache.AddWithExpiresInSecs(channelId, count, CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SEC) + } + + return count, nil +} + func (s SqlChannelStore) InvalidateGuestCount(channelId string) { channelGuestCountsCache.Remove(channelId) if s.metrics != nil { diff --git a/store/store.go b/store/store.go index 76f87fb0b0..a098317a0b 100644 --- a/store/store.go +++ b/store/store.go @@ -148,6 +148,9 @@ type ChannelStore interface { InvalidateMemberCount(channelId string) GetMemberCountFromCache(channelId string) int64 GetMemberCount(channelId string, allowFromCache bool) (int64, *model.AppError) + InvalidatePinnedPostCount(channelId string) + GetPinnedPostCountFromCache(channelId string) int64 + GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) InvalidateGuestCount(channelId string) GetGuestCountFromCache(channelId string) int64 GetGuestCount(channelId string, allowFromCache bool) (int64, *model.AppError) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index edf4531a75..e45a34ea06 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -75,6 +75,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) }) t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) + t.Run("GetPinnedPostCount", func(t *testing.T) { testChannelStoreGetPinnedPostCount(t, ss) }) t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) @@ -3379,6 +3380,78 @@ func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) { } } +func testChannelStoreGetPinnedPostCount(t *testing.T, ss store.Store) { + ch1 := &model.Channel{ + TeamId: model.NewId(), + DisplayName: "Name", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + + o1, err := ss.Channel().Save(ch1, -1) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o1.Id, + Message: "test", + IsPinned: true, + }) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o1.Id, + Message: "test", + IsPinned: true, + }) + require.Nil(t, err) + + if count, errGet := ss.Channel().GetPinnedPostCount(o1.Id, true); errGet != nil { + t.Fatal(errGet) + } else if count != 2 { + t.Fatal("didn't return right count") + } + + if ss.Channel().GetPinnedPostCountFromCache(o1.Id) != 2 { + t.Fatal("should have saved 2 pinned post count ") + } + + ch2 := &model.Channel{ + TeamId: model.NewId(), + DisplayName: "Name", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + + o2, err := ss.Channel().Save(ch2, -1) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o2.Id, + Message: "test", + }) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o2.Id, + Message: "test", + }) + require.Nil(t, err) + + if count, errGet := ss.Channel().GetPinnedPostCount(o2.Id, true); errGet != nil { + t.Fatal(errGet) + } else if count != 0 { + t.Fatal("should return 0") + } + + if ss.Channel().GetPinnedPostCountFromCache(o2.Id) != 0 { + t.Fatal("should have saved 0 pinned post count ") + } +} + func testChannelStoreMaxChannelsPerTeam(t *testing.T, ss store.Store) { channel := &model.Channel{ TeamId: model.NewId(), diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 9c78f72efd..86f258a318 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -993,6 +993,43 @@ func (_m *ChannelStore) GetMoreChannels(teamId string, userId string, offset int return r0, r1 } +// GetPinnedPostCount provides a mock function with given fields: channelId, allowFromCache +func (_m *ChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) { + ret := _m.Called(channelId, allowFromCache) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, bool) int64); ok { + r0 = rf(channelId, allowFromCache) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, bool) *model.AppError); ok { + r1 = rf(channelId, allowFromCache) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetPinnedPostCountFromCache provides a mock function with given fields: channelId +func (_m *ChannelStore) GetPinnedPostCountFromCache(channelId string) int64 { + ret := _m.Called(channelId) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string) int64); ok { + r0 = rf(channelId) + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + // GetPinnedPosts provides a mock function with given fields: channelId func (_m *ChannelStore) GetPinnedPosts(channelId string) (*model.PostList, *model.AppError) { ret := _m.Called(channelId) @@ -1139,6 +1176,11 @@ func (_m *ChannelStore) InvalidateMemberCount(channelId string) { _m.Called(channelId) } +// InvalidatePinnedPostCount provides a mock function with given fields: channelId +func (_m *ChannelStore) InvalidatePinnedPostCount(channelId string) { + _m.Called(channelId) +} + // IsUserInChannelUseCache provides a mock function with given fields: userId, channelId func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { ret := _m.Called(userId, channelId) diff --git a/store/timer_layer.go b/store/timer_layer.go index 17848f300f..da3bcb5a8c 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -1382,6 +1382,23 @@ func (s *TimerLayerChannelStore) InvalidateMemberCount(channelId string) { return } +func (s *TimerLayerChannelStore) InvalidatePinnedPostCount(channelId string) { + start := timemodule.Now() + + s.ChannelStore.InvalidatePinnedPostCount(channelId) + + t := timemodule.Now() + elapsed := t.Sub(start) + if s.Root.Metrics != nil { + success := "false" + if true { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidatePinnedPostCount", success, float64(elapsed)) + } + return +} + func (s *TimerLayerChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { start := timemodule.Now()