MM-55295: Improve DB performance of some APIs (#25318)

Load tests show that channelstore.GetMember and
channelstore.GetMembersForUser are among the chief
queries that take up CPU in the DB.

In this PR, we attempt some strategic optimizations to
reduce/optimize calls to channelstore.GetMember

1. Optimize  `(a *App) HasPermissionToChannel`

We replace GetChannelMember with GetAllChannelMembersForUser
because it's cache backed. So although it gets more data,
it does not hit the DB and saves some latency.

2. Optimize getPostsForChannelAroundLastUnread

We repace getChannelMember with getChannelMemberOnly
which is a lite version of the store call which queries
just the ChannelMembers table. This is because
in the app layer, we just use the LastViewedAt attribute.
Therefore, there is no reason to join with 5 tables when
a single table can do the job.

3. Optimize publishWebsocketEventForPermalinkPost

We use GetAllChannelMembersById instead of GetChannelMembersPage
which again joins with a lot of other tables.

4. Optimize countMentionsFromPost

Again, we replace GetChannelMember which is a costly call joining
multiple tables, with GetAllChannelMembersNotifyPropsForChannel
which is cache-backed and gives us just what we need in the app
layer - notify props.

```release-note
Make small optimizations in several DB calls:
- App.HasPermissionToChannel
- getPostsForChannelAroundLastUnread
- publishWebsocketEventForPermalinkPost
- countMentionsFromPost
```

https://mattermost.atlassian.net/browse/MM-55295

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Agniva De Sarker
2023-11-13 21:15:57 +05:30
коммит произвёл GitHub
родитель 1ed5d87342
Коммит ec88ab4ee9
12 изменённых файлов: 189 добавлений и 53 удалений

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

@@ -101,7 +101,6 @@ func (a *App) SessionHasPermissionToChannel(c request.CTX, session model.Session
}
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(session.UserId, true, true)
var channelRoles []string
if err == nil {
if roles, ok := ids[channelID]; ok {
@@ -299,17 +298,22 @@ func (a *App) HasPermissionToChannel(c request.CTX, askingUserId string, channel
return false
}
channelMember, err := a.GetChannelMember(c, channelID, askingUserId)
// We call GetAllChannelMembersForUser instead of just getting
// a single member from the DB, because it's cache backed
// and this is a very frequent call.
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(askingUserId, true, true)
var channelRoles []string
if err == nil {
roles := channelMember.GetRoles()
if a.RolesGrantPermission(roles, permission.Id) {
return true
if roles, ok := ids[channelID]; ok {
channelRoles = strings.Fields(roles)
if a.RolesGrantPermission(channelRoles, permission.Id) {
return true
}
}
}
var channel *model.Channel
channel, err = a.GetChannel(c, channelID)
if err == nil {
channel, appErr := a.GetChannel(c, channelID)
if appErr == nil {
return a.HasPermissionToTeam(c, askingUserId, channel.TeamId, permission)
}

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

@@ -2063,6 +2063,21 @@ func (s *Server) getChannelMember(c request.CTX, channelID string, userID string
return channelMember, nil
}
func (s *Server) getChannelMemberOnly(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
channelMember, err := s.Store().Channel().GetMemberOnly(c.Context(), channelID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("getChannelMemberOnly", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("getChannelMemberOnly", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channelMember, nil
}
func (a *App) GetChannelMembersPage(c request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
if err != nil {

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

@@ -785,9 +785,9 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
return false, err
}
channelMembers, err := a.GetChannelMembersPage(c, post.ChannelId, 0, 10000000)
if err != nil {
return false, err
userIDs, nErr := a.Srv().Store().Channel().GetAllChannelMemberIdsByChannelId(post.ChannelId)
if nErr != nil {
return false, model.NewAppError("publishWebsocketEventForPermalinkPost", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
permalinkPreviewedChannel, err := a.GetChannel(c, previewedPost.ChannelId)
@@ -800,18 +800,18 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
}
permalinkPreviewedPost := post.GetPreviewPost()
for _, cm := range channelMembers {
for _, userID := range userIDs {
if permalinkPreviewedPost != nil {
post.Metadata.Embeds[0].Data = permalinkPreviewedPost
}
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, cm.UserId)
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, userID)
// Using DeepCopy here to avoid a race condition
// between publishing the event and setting the "post" data value below.
messageCopy := message.DeepCopy()
broadcastCopy := messageCopy.GetBroadcast()
broadcastCopy.UserId = cm.UserId
broadcastCopy.UserId = userID
messageCopy.SetBroadcast(broadcastCopy)
postJSON, jsonErr := postForUser.ToJSON()
@@ -1260,7 +1260,7 @@ func (a *App) AddCursorIdsForPostList(originalList *model.PostList, afterPost, b
func (a *App) GetPostsForChannelAroundLastUnread(c request.CTX, channelID, userID string, limitBefore, limitAfter int, skipFetchThreads bool, collapsedThreads, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) {
var member *model.ChannelMember
var err *model.AppError
if member, err = a.GetChannelMember(c, channelID, userID); err != nil {
if member, err = a.Srv().getChannelMemberOnly(c, channelID, userID); err != nil {
return nil, err
} else if member.LastViewedAt == 0 {
return model.NewPostList(), nil
@@ -1836,9 +1836,9 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P
// countMentionsFromPost returns the number of posts in the post's channel that mention the user after and including the
// given post.
func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, int, *model.AppError) {
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return 0, 0, 0, err
channel, appErr := a.GetChannel(c, post.ChannelId)
if appErr != nil {
return 0, 0, 0, appErr
}
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
@@ -1859,15 +1859,15 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
return count, countRoot, urgentCount, nil
}
channelMember, err := a.GetChannelMember(c, channel.Id, user.Id)
members, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
if err != nil {
return 0, 0, 0, err
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
keywords := MentionKeywords{}
keywords.AddUser(
user,
channelMember.NotifyProps,
members[user.Id],
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
true, // Assume channel mentions are always allowed for simplicity
)
@@ -1877,9 +1877,9 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
// A mapping of thread root IDs to whether or not a post in that thread mentions the user
mentionedByThread := make(map[string]bool)
thread, err := a.GetPostThread(post.Id, model.GetPostsOptions{}, user.Id)
if err != nil {
return 0, 0, 0, err
thread, appErr := a.GetPostThread(post.Id, model.GetPostsOptions{}, user.Id)
if appErr != nil {
return 0, 0, 0, appErr
}
count := 0