MM-63923/MM-63924/MM-63925 Prevent deadlocks and constraint errors in UpdateSidebarCategories (#30965)
* MM-63925 Remove most nested transactions from channel_store_categories.go There's one place which still has a nested transaction in CreateInitialSidebarCategories, but that's because it's calling out to a different part of the store. The only way to avoid that would be to break the extraction like UpdateSidebarCategories does to update preferences, but I chose not to follow that pattern here and leave it as-is. * MM-63923 Prevent deadlocks caused by updating multiple categories in a different order * MM-63923 Prevent deadlocks while deleting from SidebarChannels This could also have been resolved by sorting the categories, but combining the queries seems a bit more elegant. * MM-63924 Ensure adding SidebarChannels rows is idempotent * Add additional test to cause deadlocks * Prevent channels from appearing in a single category multiple times * Other review feedback
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
fa40a8c5d4
Коммит
b70f1d859d
@@ -5,6 +5,8 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
@@ -418,25 +420,6 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) {
|
||||
transaction, err := s.GetMaster().Beginx()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "begin_transaction")
|
||||
}
|
||||
defer finalizeTransactionX(transaction, &err)
|
||||
|
||||
result, err := s.completePopulatingCategoryChannelsT(transaction, category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = transaction.Commit(); err != nil {
|
||||
return nil, errors.Wrap(err, "commit_transaction")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
|
||||
if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites {
|
||||
return category, nil
|
||||
@@ -515,6 +498,10 @@ func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, cate
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, error) {
|
||||
return s.getSidebarCategoryT(s.GetReplica(), categoryId)
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) getSidebarCategoryT(db dbSelecter, categoryId string) (*model.SidebarCategoryWithChannels, error) {
|
||||
query := s.sidebarCategorySelectQuery.
|
||||
Columns("SidebarChannels.ChannelId").
|
||||
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=SidebarCategories.Id").
|
||||
@@ -527,7 +514,7 @@ func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCa
|
||||
}
|
||||
|
||||
categories := []*sidebarCategoryForJoin{}
|
||||
if err = s.GetReplica().Select(&categories, sql, args...); err != nil {
|
||||
if err = db.Select(&categories, sql, args...); err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to get category with id=%s", categoryId))
|
||||
}
|
||||
|
||||
@@ -544,7 +531,7 @@ func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCa
|
||||
result.Channels = append(result.Channels, *category.ChannelId)
|
||||
}
|
||||
}
|
||||
return s.completePopulatingCategoryChannels(result)
|
||||
return s.completePopulatingCategoryChannelsT(db, result)
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
|
||||
@@ -630,6 +617,10 @@ func (s SqlChannelStore) GetSidebarCategories(userID string, opts *store.Sidebar
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]string, error) {
|
||||
return s.getSidebarCategoryOrderT(s.GetReplica(), userId, teamId)
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) getSidebarCategoryOrderT(db dbSelecter, userId, teamId string) ([]string, error) {
|
||||
ids := []string{}
|
||||
|
||||
sql, args, err := s.getQueryBuilder().
|
||||
@@ -645,7 +636,7 @@ func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]strin
|
||||
return nil, errors.Wrap(err, "sidebar_category_tosql")
|
||||
}
|
||||
|
||||
if err := s.GetReplica().Select(&ids, sql, args...); err != nil {
|
||||
if err := db.Select(&ids, sql, args...); err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to get category order for userId=%s, teamId=%s", userId, teamId))
|
||||
}
|
||||
|
||||
@@ -680,7 +671,7 @@ func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categ
|
||||
defer finalizeTransactionX(transaction, &err)
|
||||
|
||||
// Ensure no invalid categories are included and that no categories are left out
|
||||
existingOrder, err := s.GetSidebarCategoryOrder(userId, teamId)
|
||||
existingOrder, err := s.getSidebarCategoryOrderT(transaction, userId, teamId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -724,7 +715,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
|
||||
updatedCategories := []*model.SidebarCategoryWithChannels{}
|
||||
originalCategories := []*model.SidebarCategoryWithChannels{}
|
||||
for _, category := range categories {
|
||||
srcCategory, err2 := s.GetSidebarCategory(category.Id)
|
||||
srcCategory, err2 := s.getSidebarCategoryT(transaction, category.Id)
|
||||
if err2 != nil {
|
||||
return nil, nil, errors.Wrap(err2, "failed to find SidebarCategories")
|
||||
}
|
||||
@@ -752,11 +743,24 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
|
||||
destCategory.Muted = category.Muted
|
||||
}
|
||||
|
||||
// The order in which the queries are executed in the transaction is important.
|
||||
// SidebarCategories need to be update first, and then SidebarChannels should be deleted.
|
||||
// The net effect remains the same, but it prevents deadlocks from other transactions
|
||||
// operating on the tables in reverse order.
|
||||
updatedCategories = append(updatedCategories, destCategory)
|
||||
originalCategories = append(originalCategories, srcCategory)
|
||||
}
|
||||
|
||||
// The order in which the queries are executed in the transaction is important.
|
||||
// SidebarCategories need to be update first, and then SidebarChannels should be deleted.
|
||||
// The net effect remains the same, but it prevents deadlocks from other transactions
|
||||
// operating on the tables in reverse order.
|
||||
|
||||
// Similarly, sort the categories when updating SidebarCategories to prevent deadlocks that would occur
|
||||
// if multiple transactions were to update the table in reverse order.
|
||||
sortedUpdatedCategories := slices.Clone(updatedCategories)
|
||||
slices.SortFunc(sortedUpdatedCategories, func(a *model.SidebarCategoryWithChannels, b *model.SidebarCategoryWithChannels) int {
|
||||
return strings.Compare(a.Id, b.Id)
|
||||
})
|
||||
|
||||
// First, update the categories themselves
|
||||
for _, destCategory := range sortedUpdatedCategories {
|
||||
updateQuery, updateParams, err2 := s.getQueryBuilder().
|
||||
Update("SidebarCategories").
|
||||
Set("DisplayName", destCategory.DisplayName).
|
||||
@@ -770,49 +774,67 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
|
||||
if _, err = transaction.Exec(updateQuery, updateParams...); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to update SidebarCategories")
|
||||
}
|
||||
}
|
||||
|
||||
// if we are updating DM category, it's order can't channel order cannot be changed.
|
||||
if category.Type != model.SidebarCategoryDirectMessages {
|
||||
// Remove any SidebarChannels entries that were either:
|
||||
// - previously in this category (and any ones that are still in the category will be recreated below)
|
||||
// - in another category and are being added to this category
|
||||
query, args, err2 := s.getQueryBuilder().
|
||||
Delete("SidebarChannels").
|
||||
Where(
|
||||
sq.And{
|
||||
sq.Eq{"ChannelId": srcCategory.Channels},
|
||||
sq.Eq{"CategoryId": category.Id},
|
||||
},
|
||||
).ToSql()
|
||||
// Second, update the channels in those categories
|
||||
categoryIds := make([]string, len(categories))
|
||||
for i, category := range categories {
|
||||
categoryIds[i] = category.Id
|
||||
}
|
||||
|
||||
// Remove any SidebarChannels entries that were previously in this category. This needs to be done for all
|
||||
// categories at once to prevent deadlocks.
|
||||
//
|
||||
// Note that this means that moving channels between categories requires updating both the source and
|
||||
// destination categories.
|
||||
query, args, err2 := s.getQueryBuilder().
|
||||
Delete("SidebarChannels").
|
||||
Where(sq.Eq{"CategoryId": categoryIds}).ToSql()
|
||||
if err2 != nil {
|
||||
return nil, nil, errors.Wrap(err2, "update_sidebar_categories_tosql2")
|
||||
}
|
||||
|
||||
if _, err = transaction.Exec(query, args...); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to delete SidebarChannels")
|
||||
}
|
||||
|
||||
for _, category := range categories {
|
||||
if category.Type == model.SidebarCategoryDirectMessages {
|
||||
// The order of the DM category isn't stored explicitly, so there's nothing to do here
|
||||
continue
|
||||
}
|
||||
|
||||
runningOrder := 0
|
||||
insertQuery := s.getQueryBuilder().
|
||||
Insert("SidebarChannels").
|
||||
Columns("ChannelId", "UserId", "CategoryId", "SortOrder")
|
||||
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
insertQuery = insertQuery.Suffix("ON DUPLICATE KEY UPDATE SortOrder = VALUES(SortOrder)")
|
||||
} else {
|
||||
insertQuery = insertQuery.Suffix("ON CONFLICT (ChannelId, UserId, CategoryId) DO UPDATE SET SortOrder = excluded.SortOrder")
|
||||
}
|
||||
|
||||
for _, channelID := range category.Channels {
|
||||
insertQuery = insertQuery.Values(channelID, userId, category.Id, int64(runningOrder))
|
||||
runningOrder += model.MinimalSidebarSortDistance
|
||||
}
|
||||
|
||||
if len(category.Channels) > 0 {
|
||||
sql, args, err2 := insertQuery.ToSql()
|
||||
if err2 != nil {
|
||||
return nil, nil, errors.Wrap(err2, "update_sidebar_categories_tosql2")
|
||||
return nil, nil, errors.Wrap(err2, "InsertSidebarChannels_Tosql")
|
||||
}
|
||||
|
||||
if _, err = transaction.Exec(query, args...); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to delete SidebarChannels")
|
||||
}
|
||||
|
||||
runningOrder := 0
|
||||
insertQuery := s.getQueryBuilder().
|
||||
Insert("SidebarChannels").
|
||||
Columns("ChannelId", "UserId", "CategoryId", "SortOrder")
|
||||
for _, channelID := range category.Channels {
|
||||
insertQuery = insertQuery.Values(channelID, userId, category.Id, int64(runningOrder))
|
||||
runningOrder += model.MinimalSidebarSortDistance
|
||||
}
|
||||
|
||||
if len(category.Channels) > 0 {
|
||||
sql, args, err2 := insertQuery.ToSql()
|
||||
if err2 != nil {
|
||||
return nil, nil, errors.Wrap(err2, "InsertSidebarChannels_Tosql")
|
||||
}
|
||||
|
||||
if _, err2 := transaction.Exec(sql, args...); err2 != nil {
|
||||
return nil, nil, errors.Wrap(err2, "failed to save SidebarChannels")
|
||||
}
|
||||
if _, err2 := transaction.Exec(sql, args...); err2 != nil {
|
||||
return nil, nil, errors.Wrap(err2, "failed to save SidebarChannels")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, update preferences for Favorites
|
||||
for i, category := range categories {
|
||||
srcCategory := originalCategories[i]
|
||||
|
||||
// Update the favorites preferences based on channels moving into or out of the Favorites category for compatibility
|
||||
if category.Type == model.SidebarCategoryFavorites {
|
||||
@@ -862,9 +884,6 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
|
||||
return nil, nil, errors.Wrap(nErr, "failed to delete Preferences")
|
||||
}
|
||||
}
|
||||
|
||||
updatedCategories = append(updatedCategories, destCategory)
|
||||
originalCategories = append(originalCategories, srcCategory)
|
||||
}
|
||||
|
||||
// Ensure Channels are populated for Channels/Direct Messages category if they change
|
||||
|
||||
Ссылка в новой задаче
Block a user