[MM-53428] Delete empty drafts on upsert (#24046)

* [MM-53428] Delete empty drafts on upsert

* Add migrations to fix existing drafts

* Fix CI

* Delete empty drafts entirely from the DB

* Fix lint

* Implement batch migration for deleting drafts

* Missing store layers

* Add updated mock

* Remove unnecessary test

* PR feedback

* Add check for cluster migration

* Fix MySQL

* Don't check for len<2

* Bit of PR feedback

* Use query builder for parameters

* PR feedback

* More PR feedback

* Merge'd

* unit test GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration

* simplified builder interface

* fix DeleteEmptyDraftsByCreateAtAndUserId for MySQL

* rework as batch migration worker

* fix typo

* log ip address on version mismatches too

* simplify reset semantics

* remove trace log in favour of low spam

* document parameters for clarity

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com>
Этот коммит содержится в:
Devin Binnie
2023-10-12 10:52:10 -04:00
коммит произвёл GitHub
родитель 760dfe41f9
Коммит 89492a6a46
22 изменённых файлов: 1573 добавлений и 26 удалений

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

@@ -5,6 +5,7 @@ package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -19,6 +20,8 @@ func TestDraftStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("DeleteDraft", func(t *testing.T) { testDeleteDraft(t, ss) })
t.Run("GetDraft", func(t *testing.T) { testGetDraft(t, ss) })
t.Run("GetDraftsForUser", func(t *testing.T) { testGetDraftsForUser(t, ss) })
t.Run("GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration", func(t *testing.T) { testGetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(t, ss) })
t.Run("DeleteEmptyDraftsByCreateAtAndUserId", func(t *testing.T) { testDeleteEmptyDraftsByCreateAtAndUserId(t, ss) })
}
func testSaveDraft(t *testing.T, ss store.Store) {
@@ -275,24 +278,6 @@ func testGetDraft(t *testing.T, ss store.Store) {
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
t.Run("get draft including deleted", func(t *testing.T) {
draftResp, err := ss.Draft().Get(user.Id, channel.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
err = ss.Draft().Delete(user.Id, channel.Id, "")
assert.NoError(t, err)
_, err = ss.Draft().Get(user.Id, channel.Id, "", false)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
draftResp, err = ss.Draft().Get(user.Id, channel.Id, "", true)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
})
}
func testGetDraftsForUser(t *testing.T, ss store.Store) {
@@ -351,3 +336,226 @@ func testGetDraftsForUser(t *testing.T, ss store.Store) {
assert.ElementsMatch(t, []*model.Draft{draft1, draft2}, draftResp)
})
}
func clearDrafts(t *testing.T, ss store.Store) {
t.Helper()
_, err := ss.GetInternalMasterDB().Exec("DELETE FROM Drafts")
require.NoError(t, err)
}
func makeDrafts(t *testing.T, ss store.Store, count int, message string) {
t.Helper()
var delay time.Duration
if count > 100 {
// When creating more than one page of drafts, improve the odds we get
// some results with different CreateAt timetsamps.
delay = 5 * time.Millisecond
}
for i := 1; i <= count; i++ {
_, err := ss.Draft().Upsert(&model.Draft{
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: message,
})
require.NoError(t, err)
if delay > 0 {
time.Sleep(delay)
}
}
}
func countDraftPages(t *testing.T, ss store.Store) int {
t.Helper()
pages := 0
createAt := int64(0)
userId := ""
for {
nextCreateAt, nextUserId, err := ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
if nextCreateAt == 0 && nextUserId == "" {
break
}
// Ensure we're always making progress.
if nextCreateAt == createAt {
require.Greater(t, nextUserId, userId)
} else {
require.Greater(t, nextCreateAt, createAt)
}
pages++
createAt = nextCreateAt
userId = nextUserId
}
return pages
}
func testGetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(t *testing.T, ss store.Store) {
t.Run("no drafts", func(t *testing.T) {
clearDrafts(t, ss)
createAt, userId, err := ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(0, "")
require.NoError(t, err)
assert.EqualValues(t, 0, createAt)
assert.Equal(t, "", userId)
assert.Equal(t, 0, countDraftPages(t, ss), "incorrect number of pages")
})
t.Run("single page", func(t *testing.T) {
clearDrafts(t, ss)
makeDrafts(t, ss, 100, model.NewRandomString(16))
assert.Equal(t, 1, countDraftPages(t, ss), "incorrect number of pages")
})
t.Run("multiple pages", func(t *testing.T) {
clearDrafts(t, ss)
makeDrafts(t, ss, 300, model.NewRandomString(16))
assert.Equal(t, 3, countDraftPages(t, ss), "incorrect number of pages")
})
}
func testDeleteEmptyDraftsByCreateAtAndUserId(t *testing.T, ss store.Store) {
t.Run("nil parameters", func(t *testing.T) {
clearDrafts(t, ss)
err := ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(0, "")
require.NoError(t, err)
})
t.Run("delete single page, all empty", func(t *testing.T) {
clearDrafts(t, ss)
makeDrafts(t, ss, 100, "")
createAt, userId := int64(0), ""
nextCreateAt, nextUserId, err := ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
assert.Equal(t, 0, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
assert.EqualValues(t, 0, nextCreateAt, "should have finished iterating through drafts")
assert.Equal(t, "", nextUserId, "should have finished iterating through drafts")
})
t.Run("delete multiple pages, all empty", func(t *testing.T) {
clearDrafts(t, ss)
makeDrafts(t, ss, 300, "")
createAt, userId := int64(0), ""
nextCreateAt, nextUserId, err := ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
assert.Equal(t, 2, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
assert.Equal(t, 1, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
assert.Equal(t, 0, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
assert.EqualValues(t, 0, nextCreateAt, "should have finished iterating through drafts")
assert.Equal(t, "", nextUserId, "should have finished iterating through drafts")
})
t.Run("delete multiple pages, some empty", func(t *testing.T) {
clearDrafts(t, ss)
makeDrafts(t, ss, 50, "")
makeDrafts(t, ss, 50, "message")
makeDrafts(t, ss, 50, "")
makeDrafts(t, ss, 50, "message")
makeDrafts(t, ss, 50, "")
makeDrafts(t, ss, 50, "message")
makeDrafts(t, ss, 50, "")
makeDrafts(t, ss, 50, "message")
makeDrafts(t, ss, 50, "message")
makeDrafts(t, ss, 50, "message")
// Verify initially 5 pages
assert.Equal(t, 5, countDraftPages(t, ss), "incorrect number of pages")
createAt, userId := int64(0), ""
nextCreateAt, nextUserId, err := ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
// Only deleted 50, so still 5 pages
assert.Equal(t, 5, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
// Now deleted 100, so down to 4 pages
assert.Equal(t, 4, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
// Only deleted 150 now, so still 4 pages
assert.Equal(t, 4, countDraftPages(t, ss), "incorrect number of pages")
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
// Now deleted all 200 empty messages, so down to 3 pages
assert.Equal(t, 3, countDraftPages(t, ss), "incorrect number of pages")
// Keep going through all pages to verify nothing else gets deleted.
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
err = ss.Draft().DeleteEmptyDraftsByCreateAtAndUserId(createAt, userId)
require.NoError(t, err)
createAt, userId = nextCreateAt, nextUserId
// Verify we're done iterating
nextCreateAt, nextUserId, err = ss.Draft().GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt, userId)
require.NoError(t, err)
assert.EqualValues(t, 0, nextCreateAt, "should have finished iterating through drafts")
assert.Equal(t, "", nextUserId, "should have finished iterating through drafts")
})
}

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

@@ -28,6 +28,20 @@ func (_m *DraftStore) Delete(userID string, channelID string, rootID string) err
return r0
}
// DeleteEmptyDraftsByCreateAtAndUserId provides a mock function with given fields: createAt, userId
func (_m *DraftStore) DeleteEmptyDraftsByCreateAtAndUserId(createAt int64, userId string) error {
ret := _m.Called(createAt, userId)
var r0 error
if rf, ok := ret.Get(0).(func(int64, string) error); ok {
r0 = rf(createAt, userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: userID, channelID, rootID, includeDeleted
func (_m *DraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
ret := _m.Called(userID, channelID, rootID, includeDeleted)
@@ -80,6 +94,37 @@ func (_m *DraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.D
return r0, r1
}
// GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration provides a mock function with given fields: createAt, userId
func (_m *DraftStore) GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt int64, userId string) (int64, string, error) {
ret := _m.Called(createAt, userId)
var r0 int64
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(int64, string) (int64, string, error)); ok {
return rf(createAt, userId)
}
if rf, ok := ret.Get(0).(func(int64, string) int64); ok {
r0 = rf(createAt, userId)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(int64, string) string); ok {
r1 = rf(createAt, userId)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func(int64, string) error); ok {
r2 = rf(createAt, userId)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Upsert provides a mock function with given fields: d
func (_m *DraftStore) Upsert(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)