This repository has been archived on 2024-04-06. You can view files and clone it, but cannot push or open issues or pull requests.
core-old/internal/service/player.go
Dawid Wysokiński 0107e30c0e
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
fix: fix delete logic - Player/Tribe
2022-11-13 09:08:58 +01:00

400 lines
11 KiB
Go

package service
import (
"context"
"fmt"
"sort"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/tw"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
const (
playerChunkSize = 500
playerMaxLimit = 200
playerSortMaxLen = 3
)
//counterfeiter:generate -o internal/mock/player_repository.gen.go . PlayerRepository
type PlayerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
Delete(ctx context.Context, serverKey string, ids ...int64) error
List(ctx context.Context, params domain.ListPlayersParams) ([]domain.PlayerWithRelations, int64, error)
}
//counterfeiter:generate -o internal/mock/player_getter.gen.go . PlayerGetter
type PlayerGetter interface {
GetPlayers(ctx context.Context, baseURL string) ([]tw.Player, error)
}
//counterfeiter:generate -o internal/mock/tribe_change_creator.gen.go . TribeChangeCreator
type TribeChangeCreator interface {
Create(ctx context.Context, params ...domain.CreateTribeChangeParams) error
}
type Player struct {
repo PlayerRepository
tribeChangeSvc TribeChangeCreator
client PlayerGetter
}
func NewPlayer(repo PlayerRepository, tribeChangeSvc TribeChangeCreator, client PlayerGetter) *Player {
return &Player{repo: repo, tribeChangeSvc: tribeChangeSvc, client: client}
}
func (p *Player) Refresh(ctx context.Context, key, url string) (int64, error) {
ctx, span := tracer.Start(ctx, "Player.Refresh", trace.WithAttributes(
attribute.String("server.key", key),
attribute.String("server.url", url),
))
defer span.End()
players, err := p.client.GetPlayers(ctx, url)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, fmt.Errorf("TWClient.GetPlayers: %w", err)
}
if err = p.createOrUpdate(ctx, key, players); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
}
if err = p.delete(ctx, key, players); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
}
return int64(len(players)), nil
}
func (p *Player) createOrUpdate(ctx context.Context, key string, players []tw.Player) error {
ctx, span := tracer.Start(ctx, "Player.createOrUpdate", trace.WithAttributes(
attribute.String("server.key", key),
))
defer span.End()
for i := 0; i < len(players); i += playerChunkSize {
end := i + playerChunkSize
if end > len(players) {
end = len(players)
}
if err := p.createOrUpdateChunk(ctx, key, players[i:end]); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
}
return nil
}
func (p *Player) createOrUpdateChunk(ctx context.Context, key string, chunk []tw.Player) error {
ids := make([]int64, 0, len(chunk))
for _, player := range chunk {
ids = append(ids, player.ID)
}
playersDB, _, err := p.repo.List(ctx, domain.ListPlayersParams{
ServerKeys: []string{key},
IDs: ids,
Sort: []domain.PlayerSort{
{By: domain.PlayerSortByID, Direction: domain.SortDirectionASC},
},
})
if err != nil {
return fmt.Errorf("PlayerRepository.List: %w", err)
}
tribeChanges := make([]domain.CreateTribeChangeParams, 0, countTribeChanges(chunk, playersDB))
params := make([]domain.CreatePlayerParams, 0, len(chunk))
for _, player := range chunk {
var playerDB domain.PlayerWithRelations
j := sort.Search(len(playersDB), func(i int) bool {
return playersDB[i].ID >= player.ID
})
if j < len(playersDB) && playersDB[j].ID == player.ID {
playerDB = playersDB[j]
}
if playerDB.TribeID != player.TribeID {
tribeChanges = append(tribeChanges, domain.CreateTribeChangeParams{
PlayerID: player.ID,
NewTribeID: player.TribeID,
OldTribeID: playerDB.TribeID,
ServerKey: key,
})
}
params = append(params, createPlayerParamsBuilder{player, playerDB, key}.build())
}
if err = p.repo.CreateOrUpdate(ctx, params...); err != nil {
return fmt.Errorf("PlayerRepository.CreateOrUpdate: %w", err)
}
if err = p.tribeChangeSvc.Create(ctx, tribeChanges...); err != nil {
return fmt.Errorf("TribeChangeService.Create: %w", err)
}
return nil
}
func (p *Player) delete(ctx context.Context, key string, players []tw.Player) error {
ctx, span := tracer.Start(ctx, "Player.delete", trace.WithAttributes(
attribute.String("server.key", key),
))
defer span.End()
//nolint:prealloc
var playersToDelete []int64
var tribeChanges []domain.CreateTribeChangeParams
var lastID int64
for {
playersDB, _, err := p.repo.List(ctx, domain.ListPlayersParams{
ServerKeys: []string{key},
IDGT: domain.NullInt64{
Int64: lastID,
Valid: true,
},
Deleted: domain.NullBool{
Valid: true,
Bool: false,
},
Pagination: domain.Pagination{
Limit: playerChunkSize,
},
Sort: []domain.PlayerSort{
{By: domain.PlayerSortByID, Direction: domain.SortDirectionASC},
},
})
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return fmt.Errorf("PlayerRepository.List: %w", err)
}
for _, player := range playersDB {
i := sort.Search(len(players), func(i int) bool {
return players[i].ID >= player.ID
})
if i < len(players) && players[i].ID == player.ID {
continue
}
if player.TribeID > 0 {
tribeChanges = append(tribeChanges, domain.CreateTribeChangeParams{
PlayerID: player.ID,
NewTribeID: 0,
OldTribeID: player.TribeID,
ServerKey: key,
})
}
playersToDelete = append(playersToDelete, player.ID)
}
if len(playersDB) < playerChunkSize {
break
}
lastID = playersDB[len(playersDB)-1].ID
}
if err := p.repo.Delete(ctx, key, playersToDelete...); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return fmt.Errorf("PlayerRepository.Delete: %w", err)
}
if err := p.tribeChangeSvc.Create(ctx, tribeChanges...); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return fmt.Errorf("TribeChangeService.Create: %w", err)
}
return nil
}
func (p *Player) List(ctx context.Context, params domain.ListPlayersParams) ([]domain.PlayerWithRelations, int64, error) {
ctx, span := tracer.Start(ctx, "Player.List")
defer span.End()
if len(params.Sort) == 0 {
params.Sort = []domain.PlayerSort{
{
By: domain.PlayerSortByID,
Direction: domain.SortDirectionASC,
},
}
}
if len(params.Sort) > playerSortMaxLen {
err := domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Max: playerSortMaxLen,
},
}
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, 0, err
}
if params.Pagination.Limit == 0 {
params.Pagination.Limit = playerMaxLimit
}
if err := validatePagination(params.Pagination, playerMaxLimit); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, 0, fmt.Errorf("validatePagination: %w", err)
}
players, count, err := p.repo.List(ctx, params)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, 0, fmt.Errorf("PlayerRepository.List: %w", err)
}
return players, count, nil
}
func (p *Player) GetByServerKeyAndID(
ctx context.Context,
serverKey string,
id int64,
includeTribe bool,
) (domain.PlayerWithRelations, error) {
ctx, span := tracer.Start(ctx, "Player.GetByServerKeyAndID", trace.WithAttributes(
attribute.String("server.key", serverKey),
attribute.Int64("player.id", id),
))
defer span.End()
players, _, err := p.repo.List(ctx, domain.ListPlayersParams{
IDs: []int64{id},
ServerKeys: []string{serverKey},
IncludeTribe: includeTribe,
Pagination: domain.Pagination{
Limit: 1,
},
})
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return domain.PlayerWithRelations{}, fmt.Errorf("PlayerRepository.List: %w", err)
}
if len(players) == 0 {
err = domain.PlayerNotFoundError{
ID: id,
}
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return domain.PlayerWithRelations{}, err
}
return players[0], nil
}
type createPlayerParamsBuilder struct {
player tw.Player
playerDB domain.PlayerWithRelations
key string
}
func (c createPlayerParamsBuilder) build() domain.CreatePlayerParams {
var p domain.CreatePlayerParams
for _, fn := range [...]func(p domain.CreatePlayerParams) domain.CreatePlayerParams{
c.setBase,
c.setBestRank,
c.setMostVillages,
c.setMostPoints,
c.setLastActivityAt,
} {
p = fn(p)
}
return p
}
func (c createPlayerParamsBuilder) setBase(p domain.CreatePlayerParams) domain.CreatePlayerParams {
p.OpponentsDefeated = domain.OpponentsDefeated(c.player.OpponentsDefeated)
p.ID = c.player.ID
p.Name = c.player.Name
p.NumVillages = c.player.NumVillages
p.Points = c.player.Points
p.Rank = c.player.Rank
p.TribeID = c.player.TribeID
p.ProfileURL = c.player.ProfileURL
p.ServerKey = c.key
return p
}
func (c createPlayerParamsBuilder) setBestRank(p domain.CreatePlayerParams) domain.CreatePlayerParams {
if c.player.Rank >= c.playerDB.BestRank && c.playerDB.ID > 0 {
p.BestRank = c.playerDB.BestRank
p.BestRankAt = c.playerDB.BestRankAt
return p
}
p.BestRank = c.player.Rank
p.BestRankAt = time.Now()
return p
}
func (c createPlayerParamsBuilder) setMostPoints(p domain.CreatePlayerParams) domain.CreatePlayerParams {
if c.player.Points <= c.playerDB.MostPoints && c.playerDB.ID > 0 {
p.MostPoints = c.playerDB.MostPoints
p.MostPointsAt = c.playerDB.MostPointsAt
return p
}
p.MostPoints = c.player.Points
p.MostPointsAt = time.Now()
return p
}
func (c createPlayerParamsBuilder) setMostVillages(p domain.CreatePlayerParams) domain.CreatePlayerParams {
if c.player.NumVillages <= c.playerDB.MostVillages && c.playerDB.ID > 0 {
p.MostVillages = c.playerDB.MostVillages
p.MostVillagesAt = c.playerDB.MostVillagesAt
return p
}
p.MostVillages = c.player.NumVillages
p.MostVillagesAt = time.Now()
return p
}
func (c createPlayerParamsBuilder) setLastActivityAt(p domain.CreatePlayerParams) domain.CreatePlayerParams {
if c.player.Points <= c.playerDB.Points &&
c.player.NumVillages <= c.playerDB.NumVillages &&
c.player.ScoreAtt <= c.playerDB.ScoreAtt &&
c.playerDB.ID > 0 {
p.LastActivityAt = c.playerDB.LastActivityAt
return p
}
p.LastActivityAt = time.Now()
return p
}
func countTribeChanges(players []tw.Player, playersDB []domain.PlayerWithRelations) int {
tribeChangesCnt := 0
for _, player := range players {
i := sort.Search(len(playersDB), func(i int) bool {
return playersDB[i].ID >= player.ID
})
if i < len(playersDB) && playersDB[i].ID == player.ID && playersDB[i].TribeID != player.TribeID {
tribeChangesCnt++
}
}
return tribeChangesCnt
}