MM-31396: Fix a possible deadlock with SidebarCategories (#17109)
This is a deadlock due to reversed locking order of the SidebarChannels
and SidebarCategories table.
I could not find the exact culprit query from the deadlock output
because it only shows the last query a transaction is running. And from looking
at the code, the only query that runs "UPDATE SidebarCategories SET DisplayName = ?, Sorting = ? WHERE Id = ?"
is UpdateSidebarCategories. But for the deadlock to happen, it has to lock
SidebarChannels _first_, and then _then_ lock SidebarCategories.
Looking a bit more throughly, I found that DeleteSidebarCategory does indeed
lock the tables in an inverse way and if DeleteSidebarCategory runs concurrently with
UpdateSidebarCategories, they will deadlock.
Here's how it will happen.
```
tx1
DELETE FROM SidebarChannels WHERE CategoryId = 'xx';
tx2
UPDATE SidebarCategories SET DisplayName='dn' WHERE Id='xx';
tx2
DELETE FROM SidebarChannels WHERE (ChannelId IN ('yy') AND CategoryId = 'xx');
tx1
DELETE FROM SidebarCategories WHERE Id = 'xx';
```
And then we see:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
To fix this, we simply reorder the Delete query to lock the SidebarCategories first,
and then SidebarChannels.
In fact, any transaction updating/deleting rows from those two tables should always operate on that order
if possible.
https://mattermost.atlassian.net/browse/MM-31396
```release-note
Fixed a database deadlock that can happen if a sidebar category is updated and deleted at the same time.
```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c40f898bd0
Коммит
23dd4a5946
@@ -640,6 +640,11 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
|
||||
updatedCategory.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.
|
||||
|
||||
updateQuery, updateParams, _ := s.getQueryBuilder().
|
||||
Update("SidebarCategories").
|
||||
Set("DisplayName", updatedCategory.DisplayName).
|
||||
@@ -1002,30 +1007,33 @@ func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) error {
|
||||
return store.NewErrInvalidInput("SidebarCategory", "id", categoryId)
|
||||
}
|
||||
|
||||
// Delete the channels in the category
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Delete("SidebarChannels").
|
||||
Where(sq.Eq{"CategoryId": categoryId}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "delete_sidebar_cateory_tosql")
|
||||
}
|
||||
|
||||
if _, err = transaction.Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete SidebarChannel")
|
||||
}
|
||||
// The order in which the queries are executed in the transaction is important.
|
||||
// SidebarCategories need to be deleted first, and then SidebarChannels.
|
||||
// The net effect remains the same, but it prevents deadlocks from other transactions
|
||||
// operating on the tables in reverse order.
|
||||
|
||||
// Delete the category itself
|
||||
query, args, err = s.getQueryBuilder().
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Delete("SidebarCategories").
|
||||
Where(sq.Eq{"Id": categoryId}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "delete_sidebar_cateory_tosql")
|
||||
}
|
||||
|
||||
if _, err = transaction.Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete SidebarCategory")
|
||||
}
|
||||
|
||||
// Delete the channels in the category
|
||||
query, args, err = s.getQueryBuilder().
|
||||
Delete("SidebarChannels").
|
||||
Where(sq.Eq{"CategoryId": categoryId}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "delete_sidebar_cateory_tosql")
|
||||
}
|
||||
if _, err = transaction.Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete SidebarChannel")
|
||||
}
|
||||
|
||||
if err := transaction.Commit(); err != nil {
|
||||
return errors.Wrap(err, "commit_transaction")
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user