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
}
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().
Model(&model.Group{}).
Returning("NULL").
Where("id IN (?)", id).
Where("id IN (?)", bun.In(ids)).
Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
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
}

View File

@ -211,6 +211,44 @@ func TestGroup_List(t *testing.T) {
"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 {
@ -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 {
tb.Helper()
//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))
for _, id := range ids {

View File

@ -28,6 +28,22 @@
channel_gains: 1555
channel_losses: 1235
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
rows:
- _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)
SetInternals(ctx context.Context, id, serverID string, internals 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)
Delete(ctx context.Context, id, serverID string) error
}
@ -122,14 +123,27 @@ func (b *Bot) registerCommand(cmd command) error {
}
func (b *Bot) initCron() error {
_, err := b.c.AddJob("@every 1m", &executeMonitorsJob{
svc: b.monitorSvc,
s: b.s,
logger: b.logger,
})
if err != nil {
return err
jobs := []struct {
spec string
job cron.Job
}{
{
spec: "@every 1m",
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
}

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
}
type ListGroupsParamsTWServer struct {
VersionCode string
ServerKey string
}
type ListGroupsParams struct {
ServerIDs []string // DC server IDs
Servers []ListGroupsParamsTWServer // version codes + tw server keys
EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null
ServerIDs []string // DC server IDs
VersionCode NullString
ServerKeys []string
EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null
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 {
if err := g.cleanUpOldWithDisabledNotifications(ctx); err != nil {
if err := g.deleteAllWithDisabledNotifications(ctx); err != nil {
return err
}
if err := g.cleanUpOldWithClosedTWServers(ctx); err != nil {
if err := g.deleteAllWithClosedTWServers(ctx); err != nil {
return err
}
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{
EnabledNotifications: domain.NullBool{
Bool: false,
@ -190,7 +190,7 @@ func (g *Group) cleanUpOldWithDisabledNotifications(ctx context.Context) error {
return nil
}
func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error {
func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
versions, err := g.client.ListVersions(ctx)
if err != nil {
return fmt.Errorf("TWHelpClient.ListVersions: %w", err)
@ -208,14 +208,19 @@ func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error {
continue
}
if len(servers) == 0 {
continue
}
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 {
params.Servers = append(params.Servers, domain.ListGroupsParamsTWServer{
VersionCode: v.Code,
ServerKey: s.Key,
})
params.ServerKeys = append(params.ServerKeys, s.Key)
}
groups, err := g.repo.List(ctx, params)
@ -224,6 +229,10 @@ func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error {
continue
}
if len(groups) == 0 {
continue
}
ids := make([]string, 0, len(groups))
for _, group := range groups {
ids = append(ids, group.ID)

View File

@ -132,3 +132,166 @@ func TestGroup_Create(t *testing.T) {
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())
})
}