break up getThreadsForUser, leverage errgroup (#19709)

* MM-42282: handle teamId parameter correctly

As per https://community-daily.mattermost.com/core/pl/ugs7ue6e4j8a7cgegk1bxje8to, `ThreadStore.GetThreadsForUser` accepts a `teamId` parameter, but incorrectly handles an empty value of `""` as looking only for channels with an empty `teamId` (aka DMs and GMs) instead of finding all channels and effectively ignoring the team property.

Fixes: https://mattermost.atlassian.net/browse/MM-42282

* break up getThreadsForUser, leverage errgroup

This change breaks up `GetThreadsForUser` in the `ThreadStore` into its constituent `GetTotalUnreadThreads`, `GetTotalThreads`, `GetTotalUnreadMentions`, and the original `GetThreadsForUser` but now solely returning the thread structures. Instead of a monolithic method at the store level, the application layer now handles calling bulk requests, leveraging `errgroup` for simpler parallelization.

This change brings with it a few benefits:
* Simpler code, including more idiomatic usage of squirrel
* Simpler SQL, joining tables only when configured conditions require same. (No performance benefit here, since an unused LEFT JOIN generally has no overhead.)
* Discrete Grafana metrics for each store method, giving us better insight into the performance characteristics in play.
* **Performance boost**: reduced overhead when clearing push notifications.

This last point is what prompted the re-re-reactoring in this PR. As I broke things up, I realized that `clearPushNotificationSync` only used the `TotalUnreadMentions`, but asked for the count of total threads and total unread threads. By exposing the discrete methods, this code path avoids two aggregate queries. We clear notifications when marking a thread as read, and when marking a channel with unread mentions as viewed, so I expect we'll see at least a modest boost to performance from simply not wasting these cycles anymore.

No performance improvements are expected from this PR for the general case of using `GetThreadsForUser` to populate the threads view.

* never discard errors from building queries

* no MustSql

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Jesse Hallam
2022-03-15 10:29:00 -03:00
коммит произвёл GitHub
родитель 4bf3d6a7f5
Коммит 47c44a9b7d
10 изменённых файлов: 850 добавлений и 307 удалений

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

@@ -236,11 +236,11 @@ func (a *App) clearPushNotificationSync(currentSessionId, userID, channelID, roo
msg.Badge = int(unreadCount)
if msg.IsCRTEnabled {
data, err := a.Srv().Store.Thread().GetThreadsForUser(userID, "", model.GetUserThreadsOpts{TotalsOnly: true})
totalUnreadMentions, err := a.Srv().Store.Thread().GetTotalUnreadMentions(userID, "", model.GetUserThreadsOpts{})
if err != nil {
return model.NewAppError("clearPushNotificationSync", "app.user.get_thread_count_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
msg.Badge += int(data.TotalUnreadMentions)
msg.Badge += int(totalUnreadMentions)
}
return a.sendPushNotificationToAllSessions(msg, userID, currentSessionId)
}

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

@@ -1173,7 +1173,7 @@ func TestClearPushNotificationSync(t *testing.T) {
mockStore.On("Preference").Return(&mockPreferenceStore)
mockThreadStore := mocks.ThreadStore{}
mockThreadStore.On("GetThreadsForUser", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(&model.Threads{TotalUnreadMentions: 3}, nil)
mockThreadStore.On("GetTotalUnreadMentions", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(int64(3), nil)
mockStore.On("Thread").Return(&mockThreadStore)
err = th.App.clearPushNotificationSync(sess1.Id, "user1", "channel1", "")

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

@@ -7,7 +7,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
@@ -16,6 +15,8 @@ import (
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/app/email"
"github.com/mattermost/mattermost-server/v6/app/imaging"
"github.com/mattermost/mattermost-server/v6/app/request"
@@ -27,6 +28,7 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mfa"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store"
"golang.org/x/sync/errgroup"
)
const (
@@ -2291,15 +2293,61 @@ func (a *App) ConvertBotToUser(bot *model.Bot, userPatch *model.UserPatch, sysad
}
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
threads, err := a.Srv().Store.Thread().GetThreadsForUser(userID, teamID, options)
if err != nil {
var result model.Threads
var eg errgroup.Group
eg.Go(func() error {
totalUnreadThreads, err := a.Srv().Store.Thread().GetTotalUnreadThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count unread threads for user id=%s", userID)
}
result.TotalUnreadThreads = totalUnreadThreads
return nil
})
eg.Go(func() error {
totalCount, err := a.Srv().Store.Thread().GetTotalThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.Total = totalCount
return nil
})
eg.Go(func() error {
totalUnreadMentions, err := a.Srv().Store.Thread().GetTotalUnreadMentions(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.TotalUnreadMentions = totalUnreadMentions
return nil
})
if !options.TotalsOnly {
eg.Go(func() error {
threads, err := a.Srv().Store.Thread().GetThreadsForUser(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to get threads for user id=%s", userID)
}
result.Threads = threads
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, thread := range threads.Threads {
for _, thread := range result.Threads {
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
}
return threads, nil
return &result, nil
}
func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.ThreadMembership, *model.AppError) {