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.
```
Этот коммит содержится в:
Agniva De Sarker
2021-03-16 15:07:30 +05:30
коммит произвёл GitHub
родитель c40f898bd0
Коммит 23dd4a5946
2 изменённых файлов: 87 добавлений и 14 удалений

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

@@ -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")
}