[MM-37013] Async job to fix CRT channel unreads (#18340)
Summary The addition of the TotalMsgCountRoot and MsgCountRoot columns to support CRT caused several issues with previously read threads and channels being marked as unread. Previously we attempted to fix this purely in a SQL migration [MM-35345][MM-35494] fixes for incorrect mentions and unreads for threads and channels #17803 but that turned out to be too heavy and it was decided to break up some of the fixes into async jobs. This PR implements an async job to mark channels as read if there are no user posts since the last time the user viewed the channel. Ticket Link https://mattermost.atlassian.net/browse/MM-37013
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
12dc171a60
Коммит
0da249c651
@@ -1015,6 +1015,24 @@ func (s *OpenTracingLayerChannelStore) GetByNames(team_id string, names []string
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerChannelStore) GetCRTUnfixedChannelMembershipsAfter(channelID string, userID string, count int) ([]model.ChannelMember, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetCRTUnfixedChannelMembershipsAfter")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.ChannelStore.GetCRTUnfixedChannelMembershipsAfter(channelID, userID, count)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelCounts")
|
||||
@@ -5646,6 +5664,24 @@ func (s *OpenTracingLayerPostStore) GetSingle(id string, inclDeleted bool) (*mod
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerPostStore) GetUniquePostTypesSince(channelId string, timestamp int64) ([]string, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetUniquePostTypesSince")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.PostStore.GetUniquePostTypesSince(channelId, timestamp)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.HasAutoResponsePostByUserSince")
|
||||
|
||||
@@ -1129,6 +1129,27 @@ func (s *RetryLayerChannelStore) GetByNames(team_id string, names []string, allo
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerChannelStore) GetCRTUnfixedChannelMembershipsAfter(channelID string, userID string, count int) ([]model.ChannelMember, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.ChannelStore.GetCRTUnfixedChannelMembershipsAfter(channelID, userID, count)
|
||||
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) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
|
||||
|
||||
tries := 0
|
||||
@@ -6385,6 +6406,27 @@ func (s *RetryLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Pos
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerPostStore) GetUniquePostTypesSince(channelId string, timestamp int64) ([]string, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.PostStore.GetUniquePostTypesSince(channelId, timestamp)
|
||||
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 *RetryLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -2128,7 +2128,7 @@ func (s SqlChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID s
|
||||
|
||||
if includeTimezones {
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
selectStr += `,
|
||||
selectStr += `,
|
||||
COUNT(DISTINCT
|
||||
(
|
||||
CASE WHEN Timezone->"$.useAutomaticTimezone" = 'true' AND LENGTH(JSON_UNQUOTE(Timezone->"$.automaticTimezone")) > 0
|
||||
@@ -2138,7 +2138,7 @@ func (s SqlChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID s
|
||||
END
|
||||
)) AS ChannelMemberTimezonesCount`
|
||||
} else if s.DriverName() == model.DatabaseDriverPostgres {
|
||||
selectStr += `,
|
||||
selectStr += `,
|
||||
COUNT(DISTINCT
|
||||
(
|
||||
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
|
||||
@@ -3753,3 +3753,32 @@ func (s SqlChannelStore) GetTeamForChannel(channelID string) (*model.Team, error
|
||||
}
|
||||
return &team, nil
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) GetCRTUnfixedChannelMembershipsAfter(channelID, userID string, count int) ([]model.ChannelMember, error) {
|
||||
// we want both channelID and userID, or neither of them specified
|
||||
if (userID == "" || channelID == "") && (channelID != userID) {
|
||||
return nil, fmt.Errorf("channelID=%q userID=%q, got one empty param, both need to be empty or specified", channelID, userID)
|
||||
}
|
||||
getUnfixedCMQuery := `
|
||||
SELECT ChannelMembers.*
|
||||
FROM ChannelMembers, Channels
|
||||
WHERE ChannelId = Id AND (ChannelMembers.UserId, ChannelMembers.ChannelId) > (:userId, :channelId) AND Channels.TotalMsgCountRoot > ChannelMembers.MsgCountRoot
|
||||
ORDER BY UserId, ChannelId
|
||||
LIMIT :count;
|
||||
`
|
||||
if userID == "" && channelID == "" {
|
||||
getUnfixedCMQuery = `
|
||||
SELECT ChannelMembers.*
|
||||
FROM ChannelMembers, Channels
|
||||
WHERE ChannelId = Id AND Channels.TotalMsgCountRoot > ChannelMembers.MsgCountRoot
|
||||
ORDER BY UserId, ChannelId
|
||||
LIMIT :count;
|
||||
`
|
||||
}
|
||||
var cms []model.ChannelMember
|
||||
|
||||
if _, err := s.GetReplica().Select(&cms, getUnfixedCMQuery, map[string]interface{}{"channelId": channelID, "userId": userID, "count": count}); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to %d ChannelMembers after channelId=%q and userId=%q", count, channelID, userID)
|
||||
}
|
||||
return cms, nil
|
||||
}
|
||||
|
||||
@@ -2522,3 +2522,22 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *gorp.Transaction, pos
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUniquePostTypesSince returns the unique post types in a channel after the given timestamp
|
||||
func (s *SqlPostStore) GetUniquePostTypesSince(channelId string, timestamp int64) ([]string, error) {
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Select("DISTINCT Type").
|
||||
From("Posts").
|
||||
Where(sq.And{
|
||||
sq.Eq{"ChannelId": channelId},
|
||||
sq.GtOrEq{"CreateAt": timestamp},
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var types []string
|
||||
if _, err := s.GetReplica().Select(&types, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types, nil
|
||||
}
|
||||
|
||||
@@ -276,6 +276,9 @@ type ChannelStore interface {
|
||||
SetShared(channelId string, shared bool) error
|
||||
// GetTeamForChannel returns the team for a given channelID.
|
||||
GetTeamForChannel(channelID string) (*model.Team, error)
|
||||
|
||||
//GetCRTUnfixedChannelMembershipsAfter gets CRT unfixed channel memberships after the given channelID and userID
|
||||
GetCRTUnfixedChannelMembershipsAfter(channelID string, userID string, count int) ([]model.ChannelMember, error)
|
||||
}
|
||||
|
||||
type ChannelMemberHistoryStore interface {
|
||||
@@ -359,6 +362,9 @@ type PostStore interface {
|
||||
GetOldestEntityCreationTime() (int64, error)
|
||||
HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error)
|
||||
GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error)
|
||||
|
||||
// GetUniquePostTypesSince returns the unique post types in a channel after the given timestamp
|
||||
GetUniquePostTypesSince(channelId string, timestamp int64) ([]string, error)
|
||||
}
|
||||
|
||||
type UserStore interface {
|
||||
|
||||
@@ -582,6 +582,29 @@ func (_m *ChannelStore) GetByNames(team_id string, names []string, allowFromCach
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCRTUnfixedChannelMembershipsAfter provides a mock function with given fields: channelID, userID, count
|
||||
func (_m *ChannelStore) GetCRTUnfixedChannelMembershipsAfter(channelID string, userID string, count int) ([]model.ChannelMember, error) {
|
||||
ret := _m.Called(channelID, userID, count)
|
||||
|
||||
var r0 []model.ChannelMember
|
||||
if rf, ok := ret.Get(0).(func(string, string, int) []model.ChannelMember); ok {
|
||||
r0 = rf(channelID, userID, count)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]model.ChannelMember)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int) error); ok {
|
||||
r1 = rf(channelID, userID, count)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetChannelCounts provides a mock function with given fields: teamID, userID
|
||||
func (_m *ChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
|
||||
ret := _m.Called(teamID, userID)
|
||||
|
||||
@@ -635,6 +635,29 @@ func (_m *PostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUniquePostTypesSince provides a mock function with given fields: channelId, timestamp
|
||||
func (_m *PostStore) GetUniquePostTypesSince(channelId string, timestamp int64) ([]string, error) {
|
||||
ret := _m.Called(channelId, timestamp)
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func(string, int64) []string); ok {
|
||||
r0 = rf(channelId, timestamp)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
|
||||
r1 = rf(channelId, timestamp)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasAutoResponsePostByUserSince provides a mock function with given fields: options, userId
|
||||
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||
ret := _m.Called(options, userId)
|
||||
|
||||
@@ -949,6 +949,22 @@ func (s *TimerLayerChannelStore) GetByNames(team_id string, names []string, allo
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerChannelStore) GetCRTUnfixedChannelMembershipsAfter(channelID string, userID string, count int) ([]model.ChannelMember, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
result, err := s.ChannelStore.GetCRTUnfixedChannelMembershipsAfter(channelID, userID, count)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetCRTUnfixedChannelMembershipsAfter", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
@@ -5114,6 +5130,22 @@ func (s *TimerLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Pos
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerPostStore) GetUniquePostTypesSince(channelId string, timestamp int64) ([]string, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
result, err := s.PostStore.GetUniquePostTypesSince(channelId, timestamp)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetUniquePostTypesSince", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user