feat: auto clean up old groups

This commit is contained in:
Dawid Wysokiński 2022-10-30 06:40:16 +01:00
parent 2c6dcda547
commit 12cc652c56
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
8 changed files with 201 additions and 31 deletions

View File

@ -47,7 +47,12 @@ func New() *cli.Command {
monitorRepo := bundb.NewMonitor(db) monitorRepo := bundb.NewMonitor(db)
choiceSvc := service.NewChoice(client) choiceSvc := service.NewChoice(client)
groupSvc := service.NewGroup(groupRepo, client, cfg.MaxGroupsPerServer) groupSvc := service.NewGroup(
groupRepo,
client,
logger,
cfg.MaxGroupsPerServer,
)
monitorSvc := service.NewMonitor( monitorSvc := service.NewMonitor(
monitorRepo, monitorRepo,
groupRepo, groupRepo,

View File

@ -130,6 +130,18 @@ 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 {
_, err := g.db.NewDelete().
Model(&model.Group{}).
Returning("NULL").
Where("id IN (?)", id).
Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("couldn't delete groups: %w", err)
}
return nil
}
type updateGroupsParamsApplier struct { type updateGroupsParamsApplier struct {
params domain.UpdateGroupParams params domain.UpdateGroupParams
} }

View File

@ -105,9 +105,16 @@ 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 ServerIDs []string // DC server IDs
EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null Servers []ListGroupsParamsTWServer // version codes + tw server keys
EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null
CreatedAtLTE time.Time
} }
type GroupLimitReachedError struct { type GroupLimitReachedError struct {

View File

@ -4,9 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
"go.uber.org/zap"
) )
//counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository //counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository
@ -16,18 +18,21 @@ type GroupRepository interface {
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error)
Get(ctx context.Context, id, serverID string) (domain.Group, error) Get(ctx context.Context, id, serverID string) (domain.Group, error)
Delete(ctx context.Context, id, serverID string) error Delete(ctx context.Context, id, serverID string) error
DeleteMany(ctx context.Context, id ...string) error
} }
type Group struct { type Group struct {
repo GroupRepository repo GroupRepository
client TWHelpClient client TWHelpClient
logger *zap.Logger
maxGroupsPerServer int maxGroupsPerServer int
} }
func NewGroup(repo GroupRepository, client TWHelpClient, maxGroupsPerServer int) *Group { func NewGroup(repo GroupRepository, client TWHelpClient, logger *zap.Logger, maxGroupsPerServer int) *Group {
return &Group{ return &Group{
repo: repo, repo: repo,
client: client, client: client,
logger: logger,
maxGroupsPerServer: maxGroupsPerServer, maxGroupsPerServer: maxGroupsPerServer,
} }
} }
@ -59,6 +64,29 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do
return group, nil return group, nil
} }
func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string) error {
server, err := g.client.GetServer(ctx, versionCode, serverKey)
if err != nil {
var apiErr twhelp.APIError
if !errors.As(err, &apiErr) {
return fmt.Errorf("TWHelpClient.GetServer: %w", err)
}
return domain.ServerDoesNotExistError{
VersionCode: versionCode,
Key: serverKey,
}
}
if !server.Open {
return domain.ServerIsClosedError{
VersionCode: versionCode,
Key: serverKey,
}
}
return nil
}
func (g *Group) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error) { func (g *Group) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error) {
return g.update(ctx, id, serverID, domain.UpdateGroupParams{ return g.update(ctx, id, serverID, domain.UpdateGroupParams{
ChannelGains: domain.NullString{ ChannelGains: domain.NullString{
@ -126,23 +154,88 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error {
return nil return nil
} }
func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string) error { func (g *Group) CleanUp(ctx context.Context) error {
server, err := g.client.GetServer(ctx, versionCode, serverKey) if err := g.cleanUpOldWithDisabledNotifications(ctx); err != nil {
if err != nil { return err
var apiErr twhelp.APIError
if !errors.As(err, &apiErr) {
return fmt.Errorf("TWHelpClient.GetServer: %w", err)
}
return domain.ServerDoesNotExistError{
VersionCode: versionCode,
Key: serverKey,
}
} }
if !server.Open { if err := g.cleanUpOldWithClosedTWServers(ctx); err != nil {
return domain.ServerIsClosedError{ return err
VersionCode: versionCode, }
Key: serverKey,
return nil
}
func (g *Group) cleanUpOldWithDisabledNotifications(ctx context.Context) error {
groups, err := g.repo.List(ctx, domain.ListGroupsParams{
EnabledNotifications: domain.NullBool{
Bool: false,
Valid: true,
},
CreatedAtLTE: time.Now().Add(-24 * time.Hour),
})
if err != nil {
return fmt.Errorf("GroupRepository.List: %w", err)
}
ids := make([]string, 0, len(groups))
for _, group := range groups {
ids = append(ids, group.ID)
}
if err = g.repo.DeleteMany(ctx, ids...); err != nil {
return fmt.Errorf("GroupRepository.DeleteMany: %w", err)
}
return nil
}
func (g *Group) cleanUpOldWithClosedTWServers(ctx context.Context) error {
versions, err := g.client.ListVersions(ctx)
if err != nil {
return fmt.Errorf("TWHelpClient.ListVersions: %w", err)
}
for _, v := range versions {
servers, err := g.client.ListServers(ctx, v.Code, twhelp.ListServersQueryParams{
Open: twhelp.NullBool{
Bool: false,
Valid: true,
},
})
if err != nil {
g.logger.Warn("failed to fetch closed servers", zap.Error(err), zap.String("versionCode", v.Code))
continue
}
params := domain.ListGroupsParams{
Servers: make([]domain.ListGroupsParamsTWServer, 0, len(servers)),
}
for _, s := range servers {
params.Servers = append(params.Servers, domain.ListGroupsParamsTWServer{
VersionCode: v.Code,
ServerKey: s.Key,
})
}
groups, err := g.repo.List(ctx, params)
if err != nil {
g.logger.Warn("failed to list groups", zap.Error(err), zap.String("versionCode", v.Code))
continue
}
ids := make([]string, 0, len(groups))
for _, group := range groups {
ids = append(ids, group.ID)
}
if err = g.repo.DeleteMany(ctx, ids...); err != nil {
g.logger.Warn(
"failed to delete groups",
zap.Error(err),
zap.String("versionCode", v.Code),
)
continue
} }
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap"
) )
func TestGroup_Create(t *testing.T) { func TestGroup_Create(t *testing.T) {
@ -57,7 +58,7 @@ func TestGroup_Create(t *testing.T) {
}, nil }, nil
}) })
g, err := service.NewGroup(repo, client, 1).Create(context.Background(), params) g, err := service.NewGroup(repo, client, zap.NewNop(), 1).Create(context.Background(), params)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, g.ID) assert.NotEmpty(t, g.ID)
assert.Equal(t, params.ServerID(), g.ServerID) assert.Equal(t, params.ServerID(), g.ServerID)
@ -78,7 +79,7 @@ func TestGroup_Create(t *testing.T) {
repo := &mock.FakeGroupRepository{} repo := &mock.FakeGroupRepository{}
repo.ListReturns(make([]domain.Group, maxGroupsPerServer), nil) repo.ListReturns(make([]domain.Group, maxGroupsPerServer), nil)
g, err := service.NewGroup(repo, nil, maxGroupsPerServer).Create(context.Background(), params) g, err := service.NewGroup(repo, nil, zap.NewNop(), maxGroupsPerServer).Create(context.Background(), params)
assert.ErrorIs(t, err, domain.GroupLimitReachedError{ assert.ErrorIs(t, err, domain.GroupLimitReachedError{
Current: maxGroupsPerServer, Current: maxGroupsPerServer,
Limit: maxGroupsPerServer, Limit: maxGroupsPerServer,
@ -100,7 +101,7 @@ func TestGroup_Create(t *testing.T) {
} }
}) })
g, err := service.NewGroup(repo, client, 1).Create(context.Background(), params) g, err := service.NewGroup(repo, client, zap.NewNop(), 1).Create(context.Background(), params)
assert.ErrorIs(t, err, domain.ServerDoesNotExistError{ assert.ErrorIs(t, err, domain.ServerDoesNotExistError{
VersionCode: params.VersionCode(), VersionCode: params.VersionCode(),
Key: params.ServerKey(), Key: params.ServerKey(),
@ -123,7 +124,7 @@ func TestGroup_Create(t *testing.T) {
}, nil }, nil
}) })
g, err := service.NewGroup(repo, client, 1).Create(context.Background(), params) g, err := service.NewGroup(repo, client, zap.NewNop(), 1).Create(context.Background(), params)
assert.ErrorIs(t, err, domain.ServerIsClosedError{ assert.ErrorIs(t, err, domain.ServerIsClosedError{
VersionCode: params.VersionCode(), VersionCode: params.VersionCode(),
Key: params.ServerKey(), Key: params.ServerKey(),

View File

@ -11,6 +11,11 @@ import (
//counterfeiter:generate -o internal/mock/twhelp_client.gen.go . TWHelpClient //counterfeiter:generate -o internal/mock/twhelp_client.gen.go . TWHelpClient
type TWHelpClient interface { type TWHelpClient interface {
ListVersions(ctx context.Context) ([]twhelp.Version, error) ListVersions(ctx context.Context) ([]twhelp.Version, error)
ListServers(
ctx context.Context,
version string,
params twhelp.ListServersQueryParams,
) ([]twhelp.Server, error)
GetServer(ctx context.Context, version, server string) (twhelp.Server, error) GetServer(ctx context.Context, version, server string) (twhelp.Server, error)
GetTribeByID(ctx context.Context, version, server string, id int64) (twhelp.Tribe, error) GetTribeByID(ctx context.Context, version, server string, id int64) (twhelp.Tribe, error)
GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error) GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error)

View File

@ -16,6 +16,7 @@ const (
defaultTimeout = 10 * time.Second defaultTimeout = 10 * time.Second
endpointListVersions = "/api/v1/versions" endpointListVersions = "/api/v1/versions"
endpointListServers = "/api/v1/versions/%s/servers"
endpointGetServer = "/api/v1/versions/%s/servers/%s" endpointGetServer = "/api/v1/versions/%s/servers/%s"
endpointGetTribeByID = "/api/v1/versions/%s/servers/%s/tribes/%d" endpointGetTribeByID = "/api/v1/versions/%s/servers/%s/tribes/%d"
endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s" endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s"
@ -64,6 +65,39 @@ func (c *Client) ListVersions(ctx context.Context) ([]Version, error) {
return resp.Data, nil return resp.Data, nil
} }
type ListServersQueryParams struct {
Limit int32
Offset int32
Open NullBool
}
func (c *Client) ListServers(
ctx context.Context,
version string,
params ListServersQueryParams,
) ([]Server, error) {
q := url.Values{}
if params.Limit > 0 {
q.Set("limit", strconv.Itoa(int(params.Limit)))
}
if params.Offset > 0 {
q.Set("offset", strconv.Itoa(int(params.Offset)))
}
if params.Open.Valid {
q.Set("open", strconv.FormatBool(params.Open.Bool))
}
var resp listServersResp
if err := c.getJSON(ctx, fmt.Sprintf(endpointListServers, version)+"?"+q.Encode(), &resp); err != nil {
return nil, err
}
return resp.Data, nil
}
func (c *Client) GetServer(ctx context.Context, version, server string) (Server, error) { func (c *Client) GetServer(ctx context.Context, version, server string) (Server, error) {
var resp getServerResp var resp getServerResp
if err := c.getJSON(ctx, fmt.Sprintf(endpointGetServer, version, server), &resp); err != nil { if err := c.getJSON(ctx, fmt.Sprintf(endpointGetServer, version, server), &resp); err != nil {
@ -105,22 +139,26 @@ type ListEnnoblementsQueryParams struct {
Sort []ListEnnoblementsSort Sort []ListEnnoblementsSort
} }
func (c *Client) ListEnnoblements(ctx context.Context, version, server string, queryParams ListEnnoblementsQueryParams) ([]Ennoblement, error) { func (c *Client) ListEnnoblements(
ctx context.Context,
version, server string,
params ListEnnoblementsQueryParams,
) ([]Ennoblement, error) {
q := url.Values{} q := url.Values{}
if queryParams.Limit > 0 { if params.Limit > 0 {
q.Set("limit", strconv.Itoa(int(queryParams.Limit))) q.Set("limit", strconv.Itoa(int(params.Limit)))
} }
if queryParams.Offset > 0 { if params.Offset > 0 {
q.Set("offset", strconv.Itoa(int(queryParams.Offset))) q.Set("offset", strconv.Itoa(int(params.Offset)))
} }
if !queryParams.Since.IsZero() { if !params.Since.IsZero() {
q.Set("since", queryParams.Since.Format(time.RFC3339)) q.Set("since", params.Since.Format(time.RFC3339))
} }
for _, s := range queryParams.Sort { for _, s := range params.Sort {
q.Add("sort", s.String()) q.Add("sort", s.String())
} }

View File

@ -53,6 +53,10 @@ type getServerResp struct {
Data Server `json:"data"` Data Server `json:"data"`
} }
type listServersResp struct {
Data []Server `json:"data"`
}
type Tribe struct { type Tribe struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Tag string `json:"tag"` Tag string `json:"tag"`
@ -152,3 +156,8 @@ type Ennoblement struct {
type listEnnoblementsResp struct { type listEnnoblementsResp struct {
Data []Ennoblement `json:"data"` Data []Ennoblement `json:"data"`
} }
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}