[MM-53291] Data retention improvements (#24253)

* adding new migration for RetentionIdsForDeletion, changing logic for deleting orphaned reactions. Updating delete user and channel endpoints to remove respective reactions
Этот коммит содержится в:
Ben Cooke
2023-09-06 08:25:27 -04:00
коммит произвёл GitHub
родитель d13429aa92
Коммит 791ee40568
27 изменённых файлов: 820 добавлений и 275 удалений

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

@@ -1700,6 +1700,10 @@ func (a *App) PermanentDeleteUser(c *request.Context, user *model.User) *model.A
return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
if err := a.Srv().Store().Reaction().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.reaction.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil { if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil {
var invErr *store.ErrInvalidInput var invErr *store.ErrInvalidInput
switch { switch {

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

@@ -222,6 +222,8 @@ channels/db/migrations/mysql/000111_update_vacuuming.down.sql
channels/db/migrations/mysql/000111_update_vacuuming.up.sql channels/db/migrations/mysql/000111_update_vacuuming.up.sql
channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql
channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql
channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.down.sql
channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -444,3 +446,5 @@ channels/db/migrations/postgres/000111_update_vacuuming.down.sql
channels/db/migrations/postgres/000111_update_vacuuming.up.sql channels/db/migrations/postgres/000111_update_vacuuming.up.sql
channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql
channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql
channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.down.sql
channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.up.sql

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

@@ -0,0 +1 @@
DROP TABLE IF EXISTS RetentionIdsForDeletion;

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

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS RetentionIdsForDeletion (
Id varchar(26) NOT NULL,
TableName varchar(64),
Ids json,
PRIMARY KEY (Id),
KEY idx_retentionidsfordeletion_tablename (TableName)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

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

@@ -0,0 +1,3 @@
DROP INDEX IF EXISTS idx_retentionidsfordeletion_tablename;
DROP TABLE IF EXISTS retentionidsfordeletion;

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

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS retentionidsfordeletion (
id varchar(26) PRIMARY KEY,
tablename varchar(64),
ids varchar(26)[]
);
CREATE INDEX IF NOT EXISTS idx_retentionidsfordeletion_tablename ON retentionidsfordeletion (tablename);

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

@@ -6001,24 +6001,6 @@ func (s *OpenTracingLayerPostStore) Delete(postID string, timestamp int64, delet
return err return err
} }
func (s *OpenTracingLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { func (s *OpenTracingLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Get") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Get")
@@ -7344,22 +7326,22 @@ func (s *OpenTracingLayerReactionStore) DeleteAllWithEmojiName(emojiName string)
return err return err
} }
func (s *OpenTracingLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { func (s *OpenTracingLayerReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteOrphanedRows") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteOrphanedRowsByIds")
s.Root.Store.SetContext(newCtx) s.Root.Store.SetContext(newCtx)
defer func() { defer func() {
s.Root.Store.SetContext(origCtx) s.Root.Store.SetContext(origCtx)
}() }()
defer span.Finish() defer span.Finish()
result, err := s.ReactionStore.DeleteOrphanedRows(limit) err := s.ReactionStore.DeleteOrphanedRowsByIds(r)
if err != nil { if err != nil {
span.LogFields(spanlog.Error(err)) span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true) ext.Error.Set(span, true)
} }
return result, err return err
} }
func (s *OpenTracingLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) { func (s *OpenTracingLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
@@ -7416,6 +7398,24 @@ func (s *OpenTracingLayerReactionStore) PermanentDeleteBatch(endTime int64, limi
return result, err return result, err
} }
func (s *OpenTracingLayerReactionStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ReactionStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { func (s *OpenTracingLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Save") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Save")
@@ -7758,6 +7758,24 @@ func (s *OpenTracingLayerRetentionPolicyStore) GetCount() (int64, error) {
return result, err return result, err
} }
func (s *OpenTracingLayerRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetIdsForDeletionByTableName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetIdsForDeletionByTableName(tableName, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesCountForUser") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesCountForUser")

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

@@ -6806,27 +6806,6 @@ func (s *RetryLayerPostStore) Delete(postID string, timestamp int64, deleteByID
} }
func (s *RetryLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.PostStore.DeleteOrphanedRows(limit)
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) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { func (s *RetryLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0 tries := 0
@@ -8336,21 +8315,21 @@ func (s *RetryLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error
} }
func (s *RetryLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { func (s *RetryLayerReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error {
tries := 0 tries := 0
for { for {
result, err := s.ReactionStore.DeleteOrphanedRows(limit) err := s.ReactionStore.DeleteOrphanedRowsByIds(r)
if err == nil { if err == nil {
return result, nil return nil
} }
if !isRepeatableError(err) { if !isRepeatableError(err) {
return result, err return err
} }
tries++ tries++
if tries >= 3 { if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err return err
} }
timepkg.Sleep(100 * timepkg.Millisecond) timepkg.Sleep(100 * timepkg.Millisecond)
} }
@@ -8420,6 +8399,27 @@ func (s *RetryLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int6
} }
func (s *RetryLayerReactionStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.ReactionStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { func (s *RetryLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
tries := 0 tries := 0
@@ -8819,6 +8819,27 @@ func (s *RetryLayerRetentionPolicyStore) GetCount() (int64, error) {
} }
func (s *RetryLayerRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetIdsForDeletionByTableName(tableName, limit)
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 *RetryLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { func (s *RetryLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
tries := 0 tries := 0

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

@@ -175,6 +175,7 @@ func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(n
NowMillis: now, NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime, GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit, Limit: limit,
StoreDeletedIds: false,
}, s.SqlStore, cursor) }, s.SqlStore, cursor)
} }

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

@@ -11,6 +11,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"time"
sq "github.com/mattermost/squirrel" sq "github.com/mattermost/squirrel"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -929,24 +930,31 @@ func (s *SqlPostStore) Delete(postID string, time int64, deleteByID string) (err
return nil return nil
} }
func (s *SqlPostStore) permanentDelete(postId string) (err error) { func (s *SqlPostStore) permanentDelete(postIds []string) (err error) {
var post model.Post
transaction, err := s.GetMasterX().Beginx() transaction, err := s.GetMasterX().Beginx()
if err != nil { if err != nil {
return errors.Wrap(err, "begin_transaction") return errors.Wrap(err, "begin_transaction")
} }
defer finalizeTransactionX(transaction, &err) defer finalizeTransactionX(transaction, &err)
err = transaction.Get(&post, "SELECT * FROM Posts WHERE Id = ?", postId) if err = s.permanentDeleteThreads(transaction, postIds); err != nil {
if err != nil && err != sql.ErrNoRows { return err
return errors.Wrapf(err, "failed to get Post with id=%s", postId)
}
if err = s.permanentDeleteThreads(transaction, post.Id); err != nil {
return errors.Wrapf(err, "failed to cleanup threads for Post with id=%s", postId)
} }
if _, err = transaction.NamedExec("DELETE FROM Posts WHERE Id = :id OR RootId = :rootid", map[string]any{"id": postId, "rootid": postId}); err != nil { if err = s.permanentDeleteReactions(transaction, postIds); err != nil {
return errors.Wrapf(err, "failed to delete Post with id=%s", postId) return err
}
query := s.getQueryBuilder().
Delete("Posts").
Where(
sq.Or{
sq.Eq{"Id": postIds},
sq.Eq{"RootId": postIds},
},
)
if _, err = transaction.ExecBuilder(query); err != nil {
return errors.Wrap(err, "failed to delete Posts")
} }
if err = transaction.Commit(); err != nil { if err = transaction.Commit(); err != nil {
@@ -980,10 +988,17 @@ func (s *SqlPostStore) permanentDeleteAllCommentByUser(userId string) (err error
return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId) return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId)
} }
postIds := []string{}
for _, ids := range results { for _, ids := range results {
if err = s.updateThreadAfterReplyDeletion(transaction, ids.RootId, userId); err != nil { if err = s.updateThreadAfterReplyDeletion(transaction, ids.RootId, userId); err != nil {
return err return err
} }
postIds = append(postIds, ids.Id)
}
// Delete all the reactions on the comments
if err = s.permanentDeleteReactions(transaction, postIds); err != nil {
return err
} }
if err = transaction.Commit(); err != nil { if err = transaction.Commit(); err != nil {
@@ -1005,22 +1020,20 @@ func (s *SqlPostStore) PermanentDeleteByUser(userId string) error {
// Now attempt to delete all the root posts for a user. This will also // Now attempt to delete all the root posts for a user. This will also
// delete all the comments for each post // delete all the comments for each post
found := true
count := 0 count := 0
for {
for found {
var ids []string var ids []string
err := s.GetMasterX().Select(&ids, "SELECT Id FROM Posts WHERE UserId = ? LIMIT 1000", userId) err := s.GetMasterX().Select(&ids, "SELECT Id FROM Posts WHERE UserId = ? LIMIT 1000", userId)
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to find Posts with userId=%s", userId) return errors.Wrapf(err, "failed to find Posts with userId=%s", userId)
} }
found = false if len(ids) == 0 {
for _, id := range ids { break
found = true }
if err = s.permanentDelete(id); err != nil {
return err if err = s.permanentDelete(ids); err != nil {
} return err
} }
// This is a fail safe, give up if more than 10k messages // This is a fail safe, give up if more than 10k messages
@@ -1035,6 +1048,7 @@ func (s *SqlPostStore) PermanentDeleteByUser(userId string) error {
// Permanent deletes all channel root posts and comments, // Permanent deletes all channel root posts and comments,
// deletes all threads and thread memberships // deletes all threads and thread memberships
// deletes all reactions
// no thread comment cleanup needed, since we are deleting threads and thread memberships // no thread comment cleanup needed, since we are deleting threads and thread memberships
func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) { func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) {
transaction, err := s.GetMasterX().Beginx() transaction, err := s.GetMasterX().Beginx()
@@ -1043,20 +1057,39 @@ func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) {
} }
defer finalizeTransactionX(transaction, &err) defer finalizeTransactionX(transaction, &err)
results := []postIds{} id := ""
err = transaction.Select(&results, "SELECT Id, RootId, UserId FROM Posts WHERE ChannelId = ?", channelId) for {
if err != nil { ids := []string{}
return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId) err = transaction.Select(&ids, "SELECT Id FROM Posts WHERE ChannelId = ? AND Id > ? ORDER BY Id ASC LIMIT 500", channelId, id)
} if err != nil {
return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId)
}
for _, ids := range results { if len(ids) == 0 {
if err = s.permanentDeleteThreads(transaction, ids.Id); err != nil { break
}
id = ids[len(ids)-1]
if err = s.permanentDeleteThreads(transaction, ids); err != nil {
return err return err
} }
} time.Sleep(10 * time.Millisecond)
if _, err = transaction.Exec("DELETE FROM Posts WHERE ChannelId = ?", channelId); err != nil { if err = s.permanentDeleteReactions(transaction, ids); err != nil {
return errors.Wrapf(err, "failed to delete Posts with channelId=%s", channelId) return err
}
time.Sleep(10 * time.Millisecond)
query := s.getQueryBuilder().
Delete("Posts").
Where(
sq.Eq{"Id": ids},
)
if _, err = transaction.ExecBuilder(query); err != nil {
return errors.Wrap(err, "failed to delete Posts")
}
time.Sleep(10 * time.Millisecond)
} }
if err = transaction.Commit(); err != nil { if err = transaction.Commit(); err != nil {
@@ -2419,48 +2452,10 @@ func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolic
NowMillis: now, NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime, GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit, Limit: limit,
StoreDeletedIds: true,
}, s.SqlStore, cursor) }, s.SqlStore, cursor)
} }
// DeleteOrphanedRows removes entries from Posts when a corresponding channel no longer exists.
func (s *SqlPostStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
var query string
// We need the extra level of nesting to deal with MySQL's locking
if s.DriverName() == model.DatabaseDriverMysql {
// MySQL fails to do a proper antijoin if the selecting column
// and the joining column are different. In that case, doing a subquery
// leads to a faster plan because MySQL materializes the sub-query
// and does a covering index scan on Posts table. More details on the PR with
// this commit.
query = `
DELETE FROM Posts WHERE Id IN (
SELECT * FROM (
SELECT Posts.Id FROM Posts
WHERE Posts.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX (PRIMARY))
LIMIT ?
) AS A
)`
} else {
query = `
DELETE FROM Posts WHERE Id IN (
SELECT * FROM (
SELECT Posts.Id FROM Posts
LEFT JOIN Channels ON Posts.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
}
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s *SqlPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) { func (s *SqlPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string var query string
if s.DriverName() == "postgres" { if s.DriverName() == "postgres" {
@@ -2779,16 +2774,40 @@ func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) {
} }
// Deletes a thread and a thread membership if the postId is a root post // Deletes a thread and a thread membership if the postId is a root post
func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postId string) error { func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postIds []string) error {
if _, err := transaction.Exec("DELETE FROM Threads WHERE PostId = ?", postId); err != nil { query := s.getQueryBuilder().
Delete("Threads").
Where(
sq.Eq{"PostId": postIds},
)
if _, err := transaction.ExecBuilder(query); err != nil {
return errors.Wrap(err, "failed to delete Threads") return errors.Wrap(err, "failed to delete Threads")
} }
if _, err := transaction.Exec("DELETE FROM ThreadMemberships WHERE PostId = ?", postId); err != nil {
query = s.getQueryBuilder().
Delete("ThreadMemberships").
Where(
sq.Eq{"PostId": postIds},
)
if _, err := transaction.ExecBuilder(query); err != nil {
return errors.Wrap(err, "failed to delete ThreadMemberships") return errors.Wrap(err, "failed to delete ThreadMemberships")
} }
return nil return nil
} }
func (s *SqlPostStore) permanentDeleteReactions(transaction *sqlxTxWrapper, postIds []string) error {
query := s.getQueryBuilder().
Delete("Reactions").
Where(
sq.Eq{"PostId": postIds},
)
if _, err := transaction.ExecBuilder(query); err != nil {
return errors.Wrap(err, "failed to delete Reactions")
}
return nil
}
// deleteThread marks a thread as deleted at the given time. // deleteThread marks a thread as deleted at the given time.
func (s *SqlPostStore) deleteThread(transaction *sqlxTxWrapper, postId string, deleteAtTime int64) error { func (s *SqlPostStore) deleteThread(transaction *sqlxTxWrapper, postId string, deleteAtTime int64) error {
queryString, args, err := s.getQueryBuilder(). queryString, args, err := s.getQueryBuilder().

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

@@ -4,6 +4,8 @@
package sqlstore package sqlstore
import ( import (
"time"
sq "github.com/mattermost/squirrel" sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/model"
@@ -205,24 +207,91 @@ func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) error {
return nil return nil
} }
// DeleteOrphanedRows removes entries from Reactions when a corresponding post no longer exists. func (s *SqlReactionStore) permanentDeleteReactions(userId string, postIds *[]string) error {
func (s *SqlReactionStore) DeleteOrphanedRows(limit int) (deleted int64, err error) { txn, err := s.GetMasterX().Beginx()
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM Reactions WHERE PostId IN (
SELECT * FROM (
SELECT PostId FROM Reactions
LEFT JOIN Posts ON Reactions.PostId = Posts.Id
WHERE Posts.Id IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, limit)
if err != nil { if err != nil {
return return err
} }
deleted, err = result.RowsAffected() defer finalizeTransactionX(txn, &err)
return
err = txn.Select(postIds, "SELECT PostId FROM Reactions WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to get Reactions with userId=%s", userId)
}
query := s.getQueryBuilder().
Delete("Reactions").
Where(sq.And{
sq.Eq{"PostId": postIds},
sq.Eq{"UserId": userId},
})
_, err = txn.ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to delete reactions with userId=%s", userId)
}
if err = txn.Commit(); err != nil {
return err
}
return nil
}
func (s SqlReactionStore) PermanentDeleteByUser(userId string) error {
now := model.GetMillis()
postIds := []string{}
err := s.permanentDeleteReactions(userId, &postIds)
if err != nil {
return err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return err
}
defer finalizeTransactionX(transaction, &err)
for _, postId := range postIds {
_, err = transaction.Exec(UpdatePostHasReactionsOnDeleteQuery, now, postId, postId)
if err != nil {
mlog.Warn("Unable to update Post.HasReactions while removing reactions",
mlog.String("post_id", postId),
mlog.Err(err))
}
time.Sleep(10 * time.Millisecond)
}
if err = transaction.Commit(); err != nil {
return err
}
return nil
}
func (s *SqlReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error {
txn, err := s.GetMasterX().Beginx()
if err != nil {
return err
}
defer finalizeTransactionX(txn, &err)
query := s.getQueryBuilder().
Delete("Reactions").
Where(
sq.Eq{"PostId": r.Ids},
)
_, err = txn.ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to delete orphaned reactions with RetentionIdsForDeletion Id=%s", r.Id)
}
err = deleteFromRetentionIdsTx(txn, r.Id)
if err != nil {
return err
}
if err = txn.Commit(); err != nil {
return err
}
return nil
} }
func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) { func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {

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

@@ -5,6 +5,7 @@ package sqlstore
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -817,6 +818,92 @@ func (s *SqlRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string)
return count, nil return count, nil
} }
func scanRetentionIdsForDeletion(rows *sql.Rows, isPostgres bool) ([]*model.RetentionIdsForDeletion, error) {
idsForDeletion := []*model.RetentionIdsForDeletion{}
for rows.Next() {
var row model.RetentionIdsForDeletion
if isPostgres {
if err := rows.Scan(
&row.Id, &row.TableName, pq.Array(&row.Ids),
); err != nil {
return nil, errors.Wrap(err, "unable to scan columns")
}
} else {
var ids []byte
if err := rows.Scan(
&row.Id, &row.TableName, &ids,
); err != nil {
return nil, errors.Wrap(err, "unable to scan columns")
}
if err := json.Unmarshal(ids, &row.Ids); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal ids")
}
}
idsForDeletion = append(idsForDeletion, &row)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "error while iterating over rows")
}
return idsForDeletion, nil
}
func (s *SqlRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) {
query := s.getQueryBuilder().
Select("*").
From("RetentionIdsForDeletion").
Where(
sq.Eq{"TableName": tableName},
).
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_ids_for_deletion_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to get ids for deletion")
}
defer rows.Close()
isPostgres := s.DriverName() == model.DatabaseDriverPostgres
idsForDeletion, err := scanRetentionIdsForDeletion(rows, isPostgres)
if err != nil {
return nil, errors.Wrap(err, "failed to scan ids for deletion")
}
return idsForDeletion, nil
}
func insertRetentionIdsForDeletion(txn *sqlxTxWrapper, row *model.RetentionIdsForDeletion, s *SqlStore) error {
row.PreSave()
insertBuilder := s.getQueryBuilder().
Insert("RetentionIdsForDeletion").
Columns("Id", "TableName", "Ids")
if s.DriverName() == model.DatabaseDriverPostgres {
insertBuilder = insertBuilder.
Values(row.Id, row.TableName, pq.Array(row.Ids))
} else {
jsonIds, err := json.Marshal(row.Ids)
if err != nil {
return err
}
insertBuilder = insertBuilder.
Values(row.Id, row.TableName, jsonIds)
}
insertQuery, insertArgs, err := insertBuilder.ToSql()
if err != nil {
return err
}
if _, err = txn.Exec(insertQuery, insertArgs...); err != nil {
return err
}
return nil
}
// RetentionPolicyBatchDeletionInfo gives information on how to delete records // RetentionPolicyBatchDeletionInfo gives information on how to delete records
// under a retention policy; see `genericPermanentDeleteBatchForRetentionPolicies`. // under a retention policy; see `genericPermanentDeleteBatchForRetentionPolicies`.
// //
@@ -845,6 +932,7 @@ type RetentionPolicyBatchDeletionInfo struct {
NowMillis int64 NowMillis int64
GlobalPolicyEndTime int64 GlobalPolicyEndTime int64
Limit int64 Limit int64
StoreDeletedIds bool
} }
// genericPermanentDeleteBatchForRetentionPolicies is a helper function for tables // genericPermanentDeleteBatchForRetentionPolicies is a helper function for tables
@@ -958,31 +1046,115 @@ func genericRetentionPoliciesDeletion(
if err != nil { if err != nil {
return 0, errors.Wrap(err, r.Table+"_tosql") return 0, errors.Wrap(err, r.Table+"_tosql")
} }
if s.DriverName() == model.DatabaseDriverPostgres {
primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")" if r.StoreDeletedIds {
query = ` txn, err := s.GetMasterX().Beginx()
DELETE FROM ` + r.Table + ` WHERE ` + primaryKeysStr + ` IN ( if err != nil {
` + query + ` return 0, err
)` }
} else { defer finalizeTransactionX(txn, &err)
// MySQL does not support the LIMIT clause in a subquery with IN
clauses := make([]string, len(r.PrimaryKeys)) if s.DriverName() == model.DatabaseDriverPostgres {
for i, key := range r.PrimaryKeys { primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")"
clauses[i] = r.Table + "." + key + " = A." + key
query = fmt.Sprintf("DELETE FROM %s WHERE %s IN (%s) RETURNING %s.%s", r.Table, primaryKeysStr, query, r.Table, r.PrimaryKeys[0])
var rows *sql.Rows
rows, err = txn.Query(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+r.Table)
}
defer rows.Close()
ids := []string{}
for rows.Next() {
var id string
if err = rows.Scan(&id); err != nil {
return 0, errors.Wrap(err, "unable to scan from rows")
}
ids = append(ids, id)
}
if err = rows.Err(); err != nil {
return 0, errors.Wrap(err, "failed while iterating over rows")
}
rowsAffected = int64(len(ids))
if len(ids) > 0 {
retentionIdsRow := model.RetentionIdsForDeletion{
TableName: r.Table,
Ids: ids,
}
err = insertRetentionIdsForDeletion(txn, &retentionIdsRow, s)
if err != nil {
return 0, err
}
}
} else {
retentionIdsRow := model.RetentionIdsForDeletion{
TableName: r.Table,
Ids: []string{},
}
// 1. Select rows that will be deleted
if err = txn.Select(&retentionIdsRow.Ids, query, args...); err != nil {
return 0, err
}
if len(retentionIdsRow.Ids) > 0 {
// 2. Insert selected ids into RetentionIdsForDeletion table
err = insertRetentionIdsForDeletion(txn, &retentionIdsRow, s)
if err != nil {
return 0, err
}
query = getDeleteQueriesForMySQL(r, query)
// 3. Delete from Parent table
var result sql.Result
result, err = txn.Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+r.Table)
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table)
}
}
}
if err = txn.Commit(); err != nil {
return 0, err
}
} else {
if s.DriverName() == model.DatabaseDriverPostgres {
primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")"
query = fmt.Sprintf("DELETE FROM %s WHERE %s IN (%s)", r.Table, primaryKeysStr, query)
} else {
query = getDeleteQueriesForMySQL(r, query)
}
result, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+r.Table)
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table)
} }
joinClause := strings.Join(clauses, " AND ")
query = `
DELETE ` + r.Table + ` FROM ` + r.Table + ` INNER JOIN (
` + query + `
) AS A ON ` + joinClause
}
result, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+r.Table)
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table)
} }
return return
} }
func getDeleteQueriesForMySQL(r RetentionPolicyBatchDeletionInfo, query string) string {
// MySQL does not support the LIMIT clause in a subquery with IN
clauses := make([]string, len(r.PrimaryKeys))
for i, key := range r.PrimaryKeys {
clauses[i] = r.Table + "." + key + " = A." + key
}
joinClause := strings.Join(clauses, " AND ")
return fmt.Sprintf("DELETE %s FROM %s INNER JOIN (%s) AS A ON %s", r.Table, r.Table, query, joinClause)
}
func deleteFromRetentionIdsTx(txn *sqlxTxWrapper, id string) (err error) {
if _, err := txn.Exec("DELETE FROM RetentionIdsForDeletion WHERE Id = ?", id); err != nil {
return errors.Wrap(err, "Failed to delete from RetentionIdsForDeletion")
}
return nil
}

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

@@ -919,6 +919,7 @@ func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPol
NowMillis: now, NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime, GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit, Limit: limit,
StoreDeletedIds: false,
}, s.SqlStore, cursor) }, s.SqlStore, cursor)
} }
@@ -939,66 +940,32 @@ func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolici
NowMillis: now, NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime, GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit, Limit: limit,
StoreDeletedIds: false,
}, s.SqlStore, cursor) }, s.SqlStore, cursor)
} }
// DeleteOrphanedRows removes orphaned rows from Threads and ThreadMemberships // DeleteOrphanedRows removes orphaned rows from Threads and ThreadMemberships
func (s *SqlThreadStore) DeleteOrphanedRows(limit int) (deleted int64, err error) { func (s *SqlThreadStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
var threadsQuery string
// We need the extra level of nesting to deal with MySQL's locking
if s.DriverName() == model.DatabaseDriverMysql {
// MySQL fails to do a proper antijoin if the selecting column
// and the joining column are different. In that case, doing a subquery
// leads to a faster plan because MySQL materializes the sub-query
// and does a covering index scan on Threads table. More details on the PR with
// this commit.
threadsQuery = `
DELETE FROM Threads WHERE PostId IN (
SELECT * FROM (
SELECT Threads.PostId FROM Threads
WHERE Threads.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX(PRIMARY))
LIMIT ?
) AS A
)`
} else {
threadsQuery = `
DELETE FROM Threads WHERE PostId IN (
SELECT * FROM (
SELECT Threads.PostId FROM Threads
LEFT JOIN Channels ON Threads.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
}
// We only delete a thread membership if the entire thread no longer exists, // We only delete a thread membership if the entire thread no longer exists,
// not if the root post has been deleted // not if the root post has been deleted
const threadMembershipsQuery = ` const threadMembershipsQuery = `
DELETE FROM ThreadMemberships WHERE PostId IN ( DELETE FROM ThreadMemberships WHERE PostId IN (
SELECT * FROM ( SELECT * FROM (
SELECT ThreadMemberships.PostId FROM ThreadMemberships SELECT ThreadMemberships.PostId FROM ThreadMemberships
LEFT JOIN Threads ON ThreadMemberships.PostId = Threads.PostId LEFT JOIN Threads ON ThreadMemberships.PostId = Threads.PostId
WHERE Threads.PostId IS NULL WHERE Threads.PostId IS NULL
LIMIT ? LIMIT ?
) AS A ) AS A
)` )`
result, err := s.GetMasterX().Exec(threadsQuery, limit)
result, err := s.GetMasterX().Exec(threadMembershipsQuery, limit)
if err != nil { if err != nil {
return return
} }
rpcDeleted, err := result.RowsAffected() deleted, err = result.RowsAffected()
if err != nil { if err != nil {
return return
} }
result, err = s.GetMasterX().Exec(threadMembershipsQuery, limit)
if err != nil {
return
}
rptDeleted, err := result.RowsAffected()
if err != nil {
return
}
deleted = rpcDeleted + rptDeleted
return return
} }

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

@@ -110,6 +110,7 @@ type RetentionPolicyStore interface {
GetTeamPoliciesCountForUser(userID string) (int64, error) GetTeamPoliciesCountForUser(userID string) (int64, error)
GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error) GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error)
GetChannelPoliciesCountForUser(userID string) (int64, error) GetChannelPoliciesCountForUser(userID string) (int64, error)
GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error)
} }
type TeamStore interface { type TeamStore interface {
@@ -377,7 +378,6 @@ type PostStore interface {
GetEditHistoryForPost(postId string) ([]*model.Post, error) GetEditHistoryForPost(postId string) ([]*model.Post, error)
GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error) PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetOldest() (*model.Post, error) GetOldest() (*model.Post, error)
GetMaxPostSize() int GetMaxPostSize() int
@@ -720,8 +720,9 @@ type ReactionStore interface {
GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error)
DeleteAllWithEmojiName(emojiName string) error DeleteAllWithEmojiName(emojiName string) error
BulkGetForPosts(postIds []string) ([]*model.Reaction, error) BulkGetForPosts(postIds []string) ([]*model.Reaction, error)
DeleteOrphanedRows(limit int) (int64, error) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error
PermanentDeleteBatch(endTime int64, limit int64) (int64, error) PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
PermanentDeleteByUser(userID string) error
} }
type JobStore interface { type JobStore interface {

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

@@ -389,6 +389,9 @@ func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store)
result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id) result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, result, "history should have been deleted by channel policy") require.Empty(t, result, "history should have been deleted by channel policy")
rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("ChannelMemberHistory", 1000)
require.NoError(t, err)
require.Equal(t, 0, len(rows))
} }
func testGetChannelsLeftSince(t *testing.T, ss store.Store) { func testGetChannelsLeftSince(t *testing.T, ss store.Store) {

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

@@ -113,30 +113,6 @@ func (_m *PostStore) Delete(postID string, timestamp int64, deleteByID string) e
return r0 return r0
} }
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PostStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(int) (int64, error)); ok {
return rf(limit)
}
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, id, opts, userID, sanitizeOptions // Get provides a mock function with given fields: ctx, id, opts, userID, sanitizeOptions
func (_m *PostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { func (_m *PostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(ctx, id, opts, userID, sanitizeOptions) ret := _m.Called(ctx, id, opts, userID, sanitizeOptions)

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

@@ -80,28 +80,18 @@ func (_m *ReactionStore) DeleteAllWithEmojiName(emojiName string) error {
return r0 return r0
} }
// DeleteOrphanedRows provides a mock function with given fields: limit // DeleteOrphanedRowsByIds provides a mock function with given fields: r
func (_m *ReactionStore) DeleteOrphanedRows(limit int) (int64, error) { func (_m *ReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error {
ret := _m.Called(limit) ret := _m.Called(r)
var r0 int64 var r0 error
var r1 error if rf, ok := ret.Get(0).(func(*model.RetentionIdsForDeletion) error); ok {
if rf, ok := ret.Get(0).(func(int) (int64, error)); ok { r0 = rf(r)
return rf(limit)
}
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else { } else {
r0 = ret.Get(0).(int64) r0 = ret.Error(0)
} }
if rf, ok := ret.Get(1).(func(int) error); ok { return r0
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
} }
// GetForPost provides a mock function with given fields: postID, allowFromCache // GetForPost provides a mock function with given fields: postID, allowFromCache
@@ -180,6 +170,20 @@ func (_m *ReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64
return r0, r1 return r0, r1
} }
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *ReactionStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: reaction // Save provides a mock function with given fields: reaction
func (_m *ReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { func (_m *ReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
ret := _m.Called(reaction) ret := _m.Called(reaction)

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

@@ -256,6 +256,32 @@ func (_m *RetentionPolicyStore) GetCount() (int64, error) {
return r0, r1 return r0, r1
} }
// GetIdsForDeletionByTableName provides a mock function with given fields: tableName, limit
func (_m *RetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) {
ret := _m.Called(tableName, limit)
var r0 []*model.RetentionIdsForDeletion
var r1 error
if rf, ok := ret.Get(0).(func(string, int) ([]*model.RetentionIdsForDeletion, error)); ok {
return rf(tableName, limit)
}
if rf, ok := ret.Get(0).(func(string, int) []*model.RetentionIdsForDeletion); ok {
r0 = rf(tableName, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionIdsForDeletion)
}
}
if rf, ok := ret.Get(1).(func(string, int) error); ok {
r1 = rf(tableName, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamPoliciesCountForUser provides a mock function with given fields: userID // GetTeamPoliciesCountForUser provides a mock function with given fields: userID
func (_m *RetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { func (_m *RetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
ret := _m.Called(userID) ret := _m.Called(userID)

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

@@ -1411,6 +1411,30 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {
o2, err = ss.Post().Save(o2) o2, err = ss.Post().Save(o2)
require.NoError(t, err) require.NoError(t, err)
r1 := &model.Reaction{}
r1.ChannelId = o1.ChannelId
r1.UserId = o2.UserId
r1.PostId = o1.Id
r1.EmojiName = "smile"
r1, err = ss.Reaction().Save(r1)
require.NoError(t, err)
r2 := &model.Reaction{}
r2.ChannelId = o1.ChannelId
r2.UserId = o1.UserId
r2.PostId = o2.Id
r2.EmojiName = "wave"
_, err = ss.Reaction().Save(r2)
require.NoError(t, err)
r3 := &model.Reaction{}
r3.ChannelId = o1.ChannelId
r3.UserId = model.NewId()
r3.PostId = o1.Id
r3.EmojiName = "sad"
r3, err = ss.Reaction().Save(r3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{ channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId, TeamId: teamId,
DisplayName: "DisplayName2", DisplayName: "DisplayName2",
@@ -1425,6 +1449,14 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {
o3, err = ss.Post().Save(o3) o3, err = ss.Post().Save(o3)
require.NoError(t, err) require.NoError(t, err)
r4 := &model.Reaction{}
r4.ChannelId = channel2.Id
r4.UserId = model.NewId()
r4.PostId = o3.Id
r4.EmojiName = "angry"
_, err = ss.Reaction().Save(r4)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{ channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId, TeamId: teamId,
DisplayName: "DisplayName3", DisplayName: "DisplayName3",
@@ -1475,9 +1507,21 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{}) _, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id shouldn't have failed") require.NoError(t, err, "Deleted id shouldn't have failed")
reactions, err := ss.Reaction().GetForPost(o1.Id, false)
require.NoError(t, err, "Reactions should exist")
require.Equal(t, 2, len(reactions))
emojis := []string{r1.EmojiName, r3.EmojiName}
for _, reaction := range reactions {
require.Contains(t, emojis, reaction.EmojiName)
}
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{}) _, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed") require.Error(t, err, "Deleted id should have failed")
reactions, err = ss.Reaction().GetForPost(o2.Id, false)
require.NoError(t, err, "No error for not found")
require.Equal(t, 0, len(reactions))
thread, err = ss.Thread().Get(o5.Id) thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, thread) require.NotEmpty(t, thread)
@@ -1489,6 +1533,17 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, thread) require.Nil(t, thread)
reactions, err = ss.Reaction().GetForPost(o3.Id, false)
require.NoError(t, err, "No error for not found")
require.Equal(t, 0, len(reactions))
reactions, err = ss.Reaction().GetForPost(o1.Id, false)
require.NoError(t, err, "Reactions should exist")
require.Equal(t, 2, len(reactions))
for _, reaction := range reactions {
require.Contains(t, emojis, reaction.EmojiName)
}
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{}) _, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed") require.Error(t, err, "Deleted id should have failed")
@@ -3926,8 +3981,9 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
o3, err = ss.Post().Save(o3) o3, err = ss.Post().Save(o3)
require.NoError(t, err) require.NoError(t, err)
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{}) deleted, _, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(2), deleted)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{}) _, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 1 after purge") require.Error(t, err, "Should have not found post 1 after purge")
@@ -3938,6 +3994,14 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{}) _, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Should have found post 3 after purge") require.NoError(t, err, "Should have found post 3 after purge")
rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 2, len(rows[0].Ids))
// Clean up retention ids table
err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
t.Run("with pagination", func(t *testing.T) { t.Run("with pagination", func(t *testing.T) {
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{ _, err = ss.Post().Save(&model.Post{
@@ -3950,13 +4014,31 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
} }
cursor := model.RetentionPolicyCursor{} cursor := model.RetentionPolicyCursor{}
deleted, cursor, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) deleted, cursor, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(2), deleted) require.Equal(t, int64(2), deleted)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 2, len(rows[0].Ids))
// Clean up retention ids table
err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(1), deleted) require.Equal(t, int64(1), deleted)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 1, len(rows[0].Ids))
// Clean up retention ids table
err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
}) })
t.Run("with data retention policies", func(t *testing.T) { t.Run("with data retention policies", func(t *testing.T) {
@@ -4024,6 +4106,14 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
err2 = ss.RetentionPolicy().Delete(teamPolicy.ID) err2 = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err2) require.NoError(t, err2)
// Clean up retention ids table
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
for _, row := range rows {
err = ss.Reaction().DeleteOrphanedRowsByIds(row)
require.NoError(t, err)
}
}) })
t.Run("with channel, team and global policies", func(t *testing.T) { t.Run("with channel, team and global policies", func(t *testing.T) {
@@ -4089,6 +4179,17 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{}) deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2) require.NoError(t, err2)
require.Equal(t, int64(3), deleted) require.Equal(t, int64(3), deleted)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
// Each policy would generate it's own row
require.Equal(t, 3, len(rows))
// Clean up retention ids table
for _, row := range rows {
err = ss.Reaction().DeleteOrphanedRowsByIds(row)
require.NoError(t, err)
}
}) })
} }

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

@@ -384,6 +384,14 @@ func testPreferenceDeleteOrphanedRows(t *testing.T, ss store.Store) {
_, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) _, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
assert.NoError(t, nErr) assert.NoError(t, nErr)
rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
// Clean up retention ids table
err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
_, nErr = ss.Preference().DeleteOrphanedRows(limit) _, nErr = ss.Preference().DeleteOrphanedRows(limit)
assert.NoError(t, nErr) assert.NoError(t, nErr)

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

@@ -24,6 +24,7 @@ func TestReactionStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) }) t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) })
t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, ss, s) }) t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, ss, s) })
t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) }) t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) })
t.Run("PermanentDeleteByUser", func(t *testing.T) { testPermanentDeleteByUser(t, ss) })
t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) }) t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) })
t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) }) t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) })
t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) }) t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) })
@@ -563,6 +564,91 @@ func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore
} }
func testPermanentDeleteByUser(t *testing.T, ss store.Store) {
userId := model.NewId()
post, err1 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err1)
post2, err2 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err2)
post3, err3 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err3)
reactions := []*model.Reaction{
{
UserId: userId,
PostId: post.Id,
EmojiName: "happy",
},
{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: "smile",
},
{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "joy",
},
}
for _, reaction := range reactions {
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
}
err := ss.Reaction().PermanentDeleteByUser(userId)
require.NoError(t, err)
// check that the reactions were deleted
returned, err := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, err)
require.Len(t, returned, 2, "should only have removed reaction for user")
for _, reaction := range returned {
assert.NotEqual(t, reaction.EmojiName, "happy", "should've removed reaction with emoji name")
}
returned, err = ss.Reaction().GetForPost(post2.Id, false)
require.NoError(t, err)
require.Len(t, returned, 0, "should have removed reaction for user")
returned, err = ss.Reaction().GetForPost(post3.Id, false)
require.NoError(t, err)
require.Len(t, returned, 0, "should remove reaction for user")
// check that the posts are updated
postList, err := ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions")
postList, err = ss.Post().Get(context.Background(), post2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.False(t, postList.Posts[post2.Id].HasReactions, "post shouldn't have reactions any more")
postList, err = ss.Post().Get(context.Background(), post3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more")
}
func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) { func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
const limit = 1000 const limit = 1000
team, err := ss.Team().Save(&model.Team{ team, err := ss.Team().Save(&model.Team{
@@ -620,8 +706,20 @@ func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
require.NoError(t, err) require.NoError(t, err)
_, err = ss.Reaction().DeleteOrphanedRows(limit) rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 1, len(rows[0].Ids))
require.Contains(t, rows[0].Ids, olderPost.Id)
for _, row := range rows {
err = ss.Reaction().DeleteOrphanedRowsByIds(row)
require.NoError(t, err)
}
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 0, len(rows))
returned, err := ss.Reaction().GetForPost(olderPost.Id, false) returned, err := ss.Reaction().GetForPost(olderPost.Id, false)
require.NoError(t, err) require.NoError(t, err)

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

@@ -5439,22 +5439,6 @@ func (s *TimerLayerPostStore) Delete(postID string, timestamp int64, deleteByID
return err return err
} }
func (s *TimerLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.PostStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { func (s *TimerLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now() start := time.Now()
@@ -6638,10 +6622,10 @@ func (s *TimerLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error
return err return err
} }
func (s *TimerLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { func (s *TimerLayerReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error {
start := time.Now() start := time.Now()
result, err := s.ReactionStore.DeleteOrphanedRows(limit) err := s.ReactionStore.DeleteOrphanedRowsByIds(r)
elapsed := float64(time.Since(start)) / float64(time.Second) elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil { if s.Root.Metrics != nil {
@@ -6649,9 +6633,9 @@ func (s *TimerLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
if err == nil { if err == nil {
success = "true" success = "true"
} }
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteOrphanedRows", success, elapsed) s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteOrphanedRowsByIds", success, elapsed)
} }
return result, err return err
} }
func (s *TimerLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) { func (s *TimerLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
@@ -6702,6 +6686,22 @@ func (s *TimerLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int6
return result, err return result, err
} }
func (s *TimerLayerReactionStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.ReactionStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { func (s *TimerLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
start := time.Now() start := time.Now()
@@ -7006,6 +7006,22 @@ func (s *TimerLayerRetentionPolicyStore) GetCount() (int64, error) {
return result, err return result, err
} }
func (s *TimerLayerRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetIdsForDeletionByTableName(tableName, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetIdsForDeletionByTableName", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
start := time.Now() start := time.Now()

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

@@ -6407,6 +6407,10 @@
"id": "app.reaction.get_for_post.app_error", "id": "app.reaction.get_for_post.app_error",
"translation": "Unable to get reactions for post." "translation": "Unable to get reactions for post."
}, },
{
"id": "app.reaction.permanent_delete_by_user.app_error",
"translation": "Unable to delete reactions for user."
},
{ {
"id": "app.reaction.save.save.app_error", "id": "app.reaction.save.save.app_error",
"translation": "Unable to save reaction." "translation": "Unable to save reaction."

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

@@ -832,6 +832,7 @@ func (ts *TelemetryService) trackConfig() {
"deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime, "deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime,
"batch_size": *cfg.DataRetentionSettings.BatchSize, "batch_size": *cfg.DataRetentionSettings.BatchSize,
"time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds, "time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds,
"retention_ids_batch_size": *cfg.DataRetentionSettings.RetentionIdsBatchSize,
"cleanup_jobs_threshold_days": *cfg.JobSettings.CleanupJobsThresholdDays, "cleanup_jobs_threshold_days": *cfg.JobSettings.CleanupJobsThresholdDays,
"cleanup_config_threshold_days": *cfg.JobSettings.CleanupConfigThresholdDays, "cleanup_config_threshold_days": *cfg.JobSettings.CleanupConfigThresholdDays,
}) })

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

@@ -210,6 +210,7 @@ const (
DataRetentionSettingsDefaultDeletionJobStartTime = "02:00" DataRetentionSettingsDefaultDeletionJobStartTime = "02:00"
DataRetentionSettingsDefaultBatchSize = 3000 DataRetentionSettingsDefaultBatchSize = 3000
DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds = 100 DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds = 100
DataRetentionSettingsDefaultRetentionIdsBatchSize = 100
PluginSettingsDefaultDirectory = "./plugins" PluginSettingsDefaultDirectory = "./plugins"
PluginSettingsDefaultClientDirectory = "./client/plugins" PluginSettingsDefaultClientDirectory = "./client/plugins"
@@ -2868,6 +2869,7 @@ type DataRetentionSettings struct {
DeletionJobStartTime *string `access:"compliance_data_retention_policy"` DeletionJobStartTime *string `access:"compliance_data_retention_policy"`
BatchSize *int `access:"compliance_data_retention_policy"` BatchSize *int `access:"compliance_data_retention_policy"`
TimeBetweenBatchesMilliseconds *int `access:"compliance_data_retention_policy"` TimeBetweenBatchesMilliseconds *int `access:"compliance_data_retention_policy"`
RetentionIdsBatchSize *int `access:"compliance_data_retention_policy"`
} }
func (s *DataRetentionSettings) SetDefaults() { func (s *DataRetentionSettings) SetDefaults() {
@@ -2906,6 +2908,9 @@ func (s *DataRetentionSettings) SetDefaults() {
if s.TimeBetweenBatchesMilliseconds == nil { if s.TimeBetweenBatchesMilliseconds == nil {
s.TimeBetweenBatchesMilliseconds = NewInt(DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds) s.TimeBetweenBatchesMilliseconds = NewInt(DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds)
} }
if s.RetentionIdsBatchSize == nil {
s.RetentionIdsBatchSize = NewInt(DataRetentionSettingsDefaultRetentionIdsBatchSize)
}
} }
type JobSettings struct { type JobSettings struct {

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

@@ -84,3 +84,15 @@ type RetentionPolicyCursor struct {
TeamPoliciesDone bool TeamPoliciesDone bool
GlobalPoliciesDone bool GlobalPoliciesDone bool
} }
type RetentionIdsForDeletion struct {
Id string
TableName string
Ids []string
}
func (r *RetentionIdsForDeletion) PreSave() {
if r.Id == "" {
r.Id = NewId()
}
}

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

@@ -45,8 +45,6 @@ type FeatureFlags struct {
EnableExportDirectDownload bool EnableExportDirectDownload bool
DataRetentionConcurrencyEnabled bool
StreamlinedMarketplace bool StreamlinedMarketplace bool
} }
@@ -63,7 +61,6 @@ func (f *FeatureFlags) SetDefaults() {
f.OnboardingTourTips = true f.OnboardingTourTips = true
f.CloudReverseTrial = false f.CloudReverseTrial = false
f.EnableExportDirectDownload = false f.EnableExportDirectDownload = false
f.DataRetentionConcurrencyEnabled = true
f.StreamlinedMarketplace = true f.StreamlinedMarketplace = true
} }