feat: auto clean up old groups #38

Merged
Kichiyaki merged 2 commits from feat/clean-up-old-groups into master 2022-10-31 05:52:21 +00:00
8 changed files with 351 additions and 27 deletions
Showing only changes of commit 711bbdb72b - Show all commits

View File

@ -130,11 +130,15 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error {
return nil return nil
} }
func (g *Group) DeleteMany(ctx context.Context, id ...string) error { func (g *Group) DeleteMany(ctx context.Context, ids ...string) error {
if len(ids) == 0 {
return nil
}
_, err := g.db.NewDelete(). _, err := g.db.NewDelete().
Model(&model.Group{}). Model(&model.Group{}).
Returning("NULL"). Returning("NULL").
Where("id IN (?)", id). Where("id IN (?)", bun.In(ids)).
Exec(ctx) Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) { if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("couldn't delete groups: %w", err) return fmt.Errorf("couldn't delete groups: %w", err)
@ -194,5 +198,17 @@ func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
} }
} }
if l.params.VersionCode.Valid {
q = q.Where("version_code = ?", l.params.VersionCode.String)
}
if l.params.ServerKeys != nil {
q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys))
}
if !l.params.CreatedAtLTE.IsZero() {
q = q.Where("created_at <= ?", l.params.CreatedAtLTE)
}
return q return q
} }

View File

@ -211,6 +211,44 @@ func TestGroup_List(t *testing.T) {
"abeb6c8e-70b6-445c-989f-890cd2a1f87a", "abeb6c8e-70b6-445c-989f-890cd2a1f87a",
}, },
}, },
{
name: "VersionCode=pl",
params: domain.ListGroupsParams{
VersionCode: domain.NullString{
String: "pl",
Valid: true,
},
},
expectedGroups: []string{
"d56ad37f-2637-48ea-98f8-79627f3fcc96",
"429b790e-7186-4106-b531-4cc4931ce2ba",
"abeb6c8e-70b6-445c-989f-890cd2a1f87a",
"0be82203-4ca3-4b4c-a0c8-3a70099d88f7",
"982a9765-471c-43e8-9abb-1e4ee4f03738",
},
},
{
name: "VersionCode=pl,ServerKeys=[pl180]",
params: domain.ListGroupsParams{
VersionCode: domain.NullString{
String: "pl",
Valid: true,
},
ServerKeys: []string{"pl180"},
},
expectedGroups: []string{
"982a9765-471c-43e8-9abb-1e4ee4f03738",
},
},
{
name: "CreatedAtLTE=2022-03-15T15:00:10.000Z",
params: domain.ListGroupsParams{
CreatedAtLTE: time.Date(2022, time.March, 15, 15, 00, 10, 0, time.UTC),
},
expectedGroups: []string{
"d56ad37f-2637-48ea-98f8-79627f3fcc96",
},
},
} }
for _, tt := range tests { for _, tt := range tests {
@ -363,11 +401,53 @@ func TestGroup_Delete(t *testing.T) {
}) })
} }
func TestGroup_DeleteMany(t *testing.T) {
t.Parallel()
db := newDB(t)
fixture := loadFixtures(t, db)
groupRepo := bundb.NewGroup(db)
monitorRepo := bundb.NewMonitor(db)
group1 := getGroupFromFixture(t, fixture, "group-1-server-1")
group2 := getGroupFromFixture(t, fixture, "group-2-server-1")
ids := []string{group1.ID.String(), group2.ID.String()}
serverIDs := []string{group1.ServerID}
t.Run("OK", func(t *testing.T) {
t.Parallel()
beforeDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{
ServerIDs: serverIDs,
})
assert.NoError(t, err)
assert.NoError(t, groupRepo.DeleteMany(context.Background(), ids...))
afterDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{
ServerIDs: serverIDs,
})
assert.NoError(t, err)
assert.Len(t, afterDelete, len(beforeDelete)-len(ids))
// monitors should also be deleted
for _, id := range ids {
monitors, err := monitorRepo.List(context.Background(), id)
assert.NoError(t, err)
assert.Len(t, monitors, 0)
}
})
t.Run("OK: 0 ids", func(t *testing.T) {
t.Parallel()
assert.NoError(t, groupRepo.DeleteMany(context.Background()))
})
}
func getAllGroupsFromFixture(tb testing.TB, fixture *dbfixture.Fixture) []model.Group { func getAllGroupsFromFixture(tb testing.TB, fixture *dbfixture.Fixture) []model.Group {
tb.Helper() tb.Helper()
//nolint:lll //nolint:lll
ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2"} ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2", "group-3-server-1", "group-3-server-2"}
groups := make([]model.Group, 0, len(ids)) groups := make([]model.Group, 0, len(ids))
for _, id := range ids { for _, id := range ids {

View File

@ -28,6 +28,22 @@
channel_gains: 1555 channel_gains: 1555
channel_losses: 1235 channel_losses: 1235
created_at: 2022-03-19T15:03:10.000Z created_at: 2022-03-19T15:03:10.000Z
- _id: group-3-server-1
id: 982a9765-471c-43e8-9abb-1e4ee4f03738
server_id: server-3
server_key: pl180
version_code: pl
channel_gains: 1555
channel_losses: 1235
created_at: 2022-03-23T18:03:10.000Z
- _id: group-3-server-2
id: f3a0a5d9-07fe-4770-8b43-79cbe10bb310
server_id: server-3
server_key: en130
version_code: en
channel_gains: 1555
channel_losses: 1235
created_at: 2022-04-23T18:03:10.000Z
- model: Monitor - model: Monitor
rows: rows:
- _id: monitor-1-group-1-server-1 - _id: monitor-1-group-1-server-1

View File

@ -17,6 +17,7 @@ type GroupService interface {
SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error) SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error)
SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error) SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error)
SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error)
CleanUp(ctx context.Context) error
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error)
Delete(ctx context.Context, id, serverID string) error Delete(ctx context.Context, id, serverID string) error
} }
@ -122,14 +123,27 @@ func (b *Bot) registerCommand(cmd command) error {
} }
func (b *Bot) initCron() error { func (b *Bot) initCron() error {
_, err := b.c.AddJob("@every 1m", &executeMonitorsJob{ jobs := []struct {
svc: b.monitorSvc, spec string
s: b.s, job cron.Job
logger: b.logger, }{
}) {
if err != nil { spec: "@every 1m",
return err job: &executeMonitorsJob{svc: b.monitorSvc, s: b.s, logger: b.logger},
},
{
spec: "0 */8 * * *",
job: &cleanUpGroupsJob{svc: b.groupSvc, logger: b.logger},
},
} }
for _, j := range jobs {
_, err := b.c.AddJob(j.spec, j.job)
if err != nil {
return err
}
}
return nil return nil
} }

View File

@ -0,0 +1,30 @@
package discord
import (
"context"
"time"
"go.uber.org/zap"
)
const (
cleanUpGroupsJobTimeout = 10 * time.Minute
)
type cleanUpGroupsJob struct {
svc GroupService
logger *zap.Logger
}
func (j *cleanUpGroupsJob) Run() {
ctx, cancel := context.WithTimeout(context.Background(), cleanUpGroupsJobTimeout)
defer cancel()
start := time.Now()
if err := j.svc.CleanUp(ctx); err != nil {
j.logger.Error("something went wrong while deleting old groups", zap.Error(err))
return
}
j.logger.Info("old groups have been deleted", zap.Duration("duration", time.Since(start)))
}

View File

@ -105,15 +105,11 @@ func (u UpdateGroupParams) IsZero() bool {
!u.Barbarians.Valid !u.Barbarians.Valid
} }
type ListGroupsParamsTWServer struct {
VersionCode string
ServerKey string
}
type ListGroupsParams struct { type ListGroupsParams struct {
ServerIDs []string // DC server IDs ServerIDs []string // DC server IDs
Servers []ListGroupsParamsTWServer // version codes + tw server keys VersionCode NullString
EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null ServerKeys []string
EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null
CreatedAtLTE time.Time CreatedAtLTE time.Time
} }

View File

@ -155,18 +155,18 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error {
} }
func (g *Group) CleanUp(ctx context.Context) error { func (g *Group) CleanUp(ctx context.Context) error {
if err := g.cleanUpOldWithDisabledNotifications(ctx); err != nil { if err := g.deleteAllWithDisabledNotifications(ctx); err != nil {
return err return err
} }
if err := g.cleanUpOldWithClosedTWServers(ctx); err != nil { if err := g.deleteAllWithClosedTWServers(ctx); err != nil {
return err return err
} }
return nil return nil
} }
func (g *Group) cleanUpOldWithDisabledNotifications(ctx context.Context) error { func (g *Group) deleteAllWithDisabledNotifications(ctx context.Context) error {
groups, err := g.repo.List(ctx, domain.ListGroupsParams{ groups, err := g.repo.List(ctx, domain.ListGroupsParams{
EnabledNotifications: domain.NullBool{ EnabledNotifications: domain.NullBool{
Bool: false, Bool: false,
@ -190,7 +190,7 @@ func (g *Group) cleanUpOldWithDisabledNotifications(ctx context.Context) error {
return nil return nil
} }
func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error { func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
versions, err := g.client.ListVersions(ctx) versions, err := g.client.ListVersions(ctx)
if err != nil { if err != nil {
return fmt.Errorf("TWHelpClient.ListVersions: %w", err) return fmt.Errorf("TWHelpClient.ListVersions: %w", err)
@ -208,14 +208,19 @@ func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error {
continue continue
} }
if len(servers) == 0 {
continue
}
params := domain.ListGroupsParams{ params := domain.ListGroupsParams{
Servers: make([]domain.ListGroupsParamsTWServer, 0, len(servers)), VersionCode: domain.NullString{
String: v.Code,
Valid: true,
},
ServerKeys: make([]string, 0, len(servers)),
} }
for _, s := range servers { for _, s := range servers {
params.Servers = append(params.Servers, domain.ListGroupsParamsTWServer{ params.ServerKeys = append(params.ServerKeys, s.Key)
VersionCode: v.Code,
ServerKey: s.Key,
})
} }
groups, err := g.repo.List(ctx, params) groups, err := g.repo.List(ctx, params)
@ -224,6 +229,10 @@ func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error {
continue continue
} }
if len(groups) == 0 {
continue
}
ids := make([]string, 0, len(groups)) ids := make([]string, 0, len(groups))
for _, group := range groups { for _, group := range groups {
ids = append(ids, group.ID) ids = append(ids, group.ID)

View File

@ -132,3 +132,166 @@ func TestGroup_Create(t *testing.T) {
assert.Zero(t, g) assert.Zero(t, g)
}) })
} }
func TestGroup_CleanUp(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
repo := &mock.FakeGroupRepository{}
groupsWithDisabledNotifications := []domain.Group{
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: "",
ChannelLosses: "",
Internals: true,
Barbarians: true,
ServerKey: "pl181",
VersionCode: "pl",
CreatedAt: time.Now().Add(-48 * time.Hour),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: "",
ChannelLosses: "",
Internals: true,
Barbarians: true,
ServerKey: "pl181",
VersionCode: "pl",
CreatedAt: time.Now().Add(-45 * time.Hour),
},
}
repo.ListReturnsOnCall(0, groupsWithDisabledNotifications, nil)
groupsPL := []domain.Group{
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: "",
Internals: true,
Barbarians: true,
ServerKey: "pl181",
VersionCode: "pl",
CreatedAt: time.Now().Add(-120 * time.Hour),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: uuid.NewString(),
Internals: true,
Barbarians: true,
ServerKey: "pl181",
VersionCode: "pl",
CreatedAt: time.Now().Add(-11 * time.Hour),
},
}
repo.ListReturnsOnCall(1, groupsPL, nil)
groupsUK := []domain.Group{
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: uuid.NewString(),
Internals: true,
Barbarians: true,
ServerKey: "uk51",
VersionCode: "uk",
CreatedAt: time.Now().Add(-150 * time.Hour),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: uuid.NewString(),
Internals: true,
Barbarians: true,
ServerKey: "uk52",
VersionCode: "uk",
CreatedAt: time.Now().Add(-200 * time.Hour),
},
}
repo.ListReturnsOnCall(2, groupsUK, nil)
repo.DeleteManyReturns(nil)
client := &mock.FakeTWHelpClient{}
versions := []twhelp.Version{
{
Code: "pl",
Host: "www.plemiona.pl",
Name: "Poland",
Timezone: "Europe/Warsaw",
},
{
Code: "uk",
Host: "www.tribalwars.co.uk",
Name: "United Kingdom",
Timezone: "Europe/London",
},
{
Code: "tr",
Host: "www.klanlar.org",
Name: "Turkey",
Timezone: "Europe/Istanbul",
},
}
client.ListVersionsReturns(versions, nil)
serversPL := []twhelp.Server{
{
Key: "pl181",
URL: "https://pl181.plemiona.pl",
Open: false,
},
}
client.ListServersReturnsOnCall(0, serversPL, nil)
serversUK := []twhelp.Server{
{
Key: "uk51",
URL: "https://uk51.tribalwars.co.uk",
Open: false,
},
{
Key: "uk52",
URL: "https://uk52.tribalwars.co.uk",
Open: false,
},
}
client.ListServersReturnsOnCall(1, serversUK, nil)
client.ListServersReturnsOnCall(1, serversUK, nil)
client.ListServersReturnsOnCall(2, nil, nil) // Turkey
assert.NoError(t, service.NewGroup(repo, client, zap.NewNop(), 1).CleanUp(context.Background()))
require.Equal(t, 3, repo.ListCallCount())
require.Equal(t, 3, repo.DeleteManyCallCount())
for _, tt := range []struct {
i int
groups []domain.Group
}{
{
i: 0,
groups: groupsWithDisabledNotifications,
},
{
i: 1,
groups: groupsPL,
},
{
i: 2,
groups: groupsUK,
},
} {
_, ids := repo.DeleteManyArgsForCall(tt.i)
assert.Len(t, ids, len(tt.groups))
for i, id := range ids {
assert.Equal(t, tt.groups[i].ID, id)
}
}
require.Equal(t, 1, client.ListVersionsCallCount())
require.Equal(t, 3, client.ListServersCallCount())
})
}