diff --git a/internal/app/service_server.go b/internal/app/service_server.go index 04abaf5..5180c03 100644 --- a/internal/app/service_server.go +++ b/internal/app/service_server.go @@ -28,38 +28,38 @@ func (svc *ServerService) Sync(ctx context.Context, payload domain.SyncServersCm openServers, err := svc.twSvc.GetOpenServers(ctx, payload.URL()) if err != nil { - return fmt.Errorf("couldn't get open servers for version code %s: %w", versionCode, err) + return fmt.Errorf("%s: couldn't get open servers: %w", versionCode, err) } specialServers, err := svc.listAllSpecial(ctx, versionCode) if err != nil { - return fmt.Errorf("couldn't list special servers with version code %s: %w", versionCode, err) + return fmt.Errorf("%s: couldn't list special servers: %w", versionCode, err) } currentlyStoredOpenServers, err := svc.listAllOpen(ctx, versionCode) if err != nil { - return fmt.Errorf("couldn't list open servers with version code %s: %w", versionCode, err) + return fmt.Errorf("%s: couldn't list open servers: %w", versionCode, err) } openServersWithoutSpecial := openServers.FilterOutSpecial(specialServers) serversToBeClosed, err := currentlyStoredOpenServers.Close(openServersWithoutSpecial) if err != nil { - return fmt.Errorf("couldn't close servers: %w", err) + return fmt.Errorf("%s: couldn't close servers: %w", versionCode, err) } params, err := domain.NewCreateServerParams(append(openServersWithoutSpecial, serversToBeClosed...), versionCode) if err != nil { - return err + return fmt.Errorf("%s: %w", versionCode, err) } if err = svc.repo.CreateOrUpdate(ctx, params...); err != nil { - return err + return fmt.Errorf("%s: couldn't create/update servers: %w", versionCode, err) } payloads, err := domain.NewServerSyncedEventPayloads(openServersWithoutSpecial, versionCode) if err != nil { - return fmt.Errorf("couldn't construct server synced event payloads: %w", err) + return fmt.Errorf("%s: couldn't construct server synced event payloads: %w", versionCode, err) } return svc.publisher.EventSynced(ctx, payloads...) @@ -67,11 +67,12 @@ func (svc *ServerService) Sync(ctx context.Context, payload domain.SyncServersCm func (svc *ServerService) listAllSpecial(ctx context.Context, versionCode string) (domain.Servers, error) { params := domain.NewListServersParams() - if err := params.SetVersionCodes([]string{versionCode}); err != nil { return nil, err } - + if err := params.SetSort([]domain.ServerSort{domain.ServerSortKeyASC}); err != nil { + return nil, err + } if err := params.SetSpecial(domain.NullBool{ Value: true, Valid: true, @@ -84,11 +85,12 @@ func (svc *ServerService) listAllSpecial(ctx context.Context, versionCode string func (svc *ServerService) listAllOpen(ctx context.Context, versionCode string) (domain.Servers, error) { params := domain.NewListServersParams() - if err := params.SetVersionCodes([]string{versionCode}); err != nil { return nil, err } - + if err := params.SetSort([]domain.ServerSort{domain.ServerSortKeyASC}); err != nil { + return nil, err + } if err := params.SetOpen(domain.NullBool{ Value: true, Valid: true, @@ -105,11 +107,9 @@ func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServers if err := params.SetOffset(0); err != nil { return nil, err } - if err := params.SetLimit(domain.ServerListMaxLimit); err != nil { return nil, err } - if err := params.SetSort([]domain.ServerSort{domain.ServerSortKeyASC}); err != nil { return nil, err } @@ -156,21 +156,18 @@ func (svc *ServerService) SyncConfigAndInfo(ctx context.Context, payload domain. } var updateParams domain.UpdateServerParams - if err = updateParams.SetConfig(domain.NullServerConfig{ Value: cfg, Valid: true, }); err != nil { return err } - if err = updateParams.SetBuildingInfo(domain.NullBuildingInfo{ Value: buildingInfo, Valid: true, }); err != nil { return err } - if err = updateParams.SetUnitInfo(domain.NullUnitInfo{ Value: unitInfo, Valid: true, diff --git a/internal/app/service_tribe.go b/internal/app/service_tribe.go index bda504f..253fcf9 100644 --- a/internal/app/service_tribe.go +++ b/internal/app/service_tribe.go @@ -23,12 +23,113 @@ func NewTribeService(repo TribeRepository, twSvc TWService) *TribeService { } func (svc *TribeService) Sync(ctx context.Context, payload domain.ServerSyncedEventPayload) error { + serverKey := payload.Key() + tribes, err := svc.twSvc.GetTribes(ctx, payload.URL()) if err != nil { - return fmt.Errorf("couldn't get tribes for server %s: %w", payload.Key(), err) + return fmt.Errorf("%s: couldn't get tribes: %w", serverKey, err) } - fmt.Println(payload.URL(), len(tribes)) + if err = svc.createOrUpdate(ctx, serverKey, tribes); err != nil { + return fmt.Errorf("%s: couldn't create/update tribes: %w", serverKey, err) + } + + if err = svc.delete(ctx, serverKey, tribes); err != nil { + return fmt.Errorf("%s: couldn't delete tribes: %w", serverKey, err) + } return nil } + +const tribeCreateOrUpdateChunkSize = domain.TribeListMaxLimit + +func (svc *TribeService) createOrUpdate(ctx context.Context, serverKey string, tribes domain.BaseTribes) error { + for i := 0; i < len(tribes); i += tribeCreateOrUpdateChunkSize { + end := i + tribeCreateOrUpdateChunkSize + if end > len(tribes) { + end = len(tribes) + } + + if err := svc.createOrUpdateChunk(ctx, serverKey, tribes[i:end]); err != nil { + return err + } + } + + return nil +} + +func (svc *TribeService) createOrUpdateChunk(ctx context.Context, serverKey string, tribes domain.BaseTribes) error { + ids := make([]int, 0, len(tribes)) + for _, t := range tribes { + ids = append(ids, t.ID()) + } + + listParams := domain.NewListTribesParams() + if err := listParams.SetServerKeys([]string{serverKey}); err != nil { + return err + } + if err := listParams.SetIDs(ids); err != nil { + return err + } + if err := listParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); err != nil { + return err + } + if err := listParams.SetLimit(domain.TribeListMaxLimit); err != nil { + return err + } + + storedTribes, err := svc.repo.List(ctx, listParams) + if err != nil { + return err + } + + createParams, err := domain.NewCreateTribeParams(serverKey, tribes, storedTribes) + if err != nil { + return err + } + + return svc.repo.CreateOrUpdate(ctx, createParams...) +} + +func (svc *TribeService) delete(ctx context.Context, serverKey string, tribes domain.BaseTribes) error { + listParams := domain.NewListTribesParams() + if err := listParams.SetServerKeys([]string{serverKey}); err != nil { + return err + } + if err := listParams.SetDeleted(domain.NullBool{ + Value: false, + Valid: true, + }); err != nil { + return err + } + if err := listParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); err != nil { + return err + } + if err := listParams.SetLimit(domain.TribeListMaxLimit); err != nil { + return err + } + + var toDelete []int + + for { + storedTribes, err := svc.repo.List(ctx, listParams) + if err != nil { + return err + } + + if len(storedTribes) == 0 { + break + } + + toDelete = append(toDelete, storedTribes.Delete(tribes)...) + + if err = listParams.SetIDGT(domain.NullInt{ + Value: storedTribes[len(storedTribes)-1].ID(), + Valid: true, + }); err != nil { + return err + } + } + + return svc.repo.Delete(ctx, serverKey, toDelete...) +} diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go index 14c5ef6..dee897d 100644 --- a/internal/domain/tribe.go +++ b/internal/domain/tribe.go @@ -1,6 +1,7 @@ package domain import ( + "cmp" "fmt" "math" "net/url" @@ -208,6 +209,26 @@ func (t Tribe) Base() BaseTribe { type Tribes []Tribe +// Delete finds all tribes that are not in the given slice with active tribes and returns their ids. +// Both slices must be sorted in ascending order by ID. +func (ts Tribes) Delete(active BaseTribes) []int { + //nolint:prealloc + var toDelete []int + + for _, t := range ts { + _, found := slices.BinarySearchFunc(active, t, func(a BaseTribe, b Tribe) int { + return cmp.Compare(a.ID(), b.ID()) + }) + if found { + continue + } + + toDelete = append(toDelete, t.ID()) + } + + return toDelete +} + type CreateTribeParams struct { base BaseTribe serverKey string @@ -221,6 +242,10 @@ type CreateTribeParams struct { const createTribeParamsModelName = "CreateTribeParams" +// NewCreateTribeParams constructs a slice of CreateTribeParams based on the given parameters. +// Both slices must be sorted in ascending order by ID +// + if storedTribes contains tribes from different servers. they must be sorted in ascending order by server key. +// //nolint:gocyclo func NewCreateTribeParams(serverKey string, tribes BaseTribes, storedTribes Tribes) ([]CreateTribeParams, error) { if err := validateServerKey(serverKey); err != nil { @@ -239,10 +264,14 @@ func NewCreateTribeParams(serverKey string, tribes BaseTribes, storedTribes Trib } var old Tribe - if oldIdx := slices.IndexFunc(storedTribes, func(old Tribe) bool { - return old.ID() == t.ID() && old.ServerKey() == serverKey - }); oldIdx >= 0 { - old = storedTribes[oldIdx] + idx, found := slices.BinarySearchFunc(storedTribes, t, func(a Tribe, b BaseTribe) int { + if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 { + return res + } + return cmp.Compare(a.ID(), b.ID()) + }) + if found { + old = storedTribes[idx] } p := CreateTribeParams{ diff --git a/internal/domain/tribe_test.go b/internal/domain/tribe_test.go index 7fb962e..d8bae07 100644 --- a/internal/domain/tribe_test.go +++ b/internal/domain/tribe_test.go @@ -1,6 +1,7 @@ package domain_test import ( + "cmp" "fmt" "math" "slices" @@ -14,6 +15,44 @@ import ( "github.com/stretchr/testify/require" ) +func TestTribes_Delete(t *testing.T) { + t.Parallel() + + server := domaintest.NewServer(t) + + active := domain.BaseTribes{ + domaintest.NewBaseTribe(t), + domaintest.NewBaseTribe(t), + domaintest.NewBaseTribe(t), + } + slices.SortFunc(active, func(a, b domain.BaseTribe) int { + return cmp.Compare(a.ID(), b.ID()) + }) + + tribes := domain.Tribes{ + domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) { + cfg.ID = active[0].ID() + cfg.ServerKey = server.Key() + }), + domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) { + cfg.ServerKey = server.Key() + }), + domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) { + cfg.ID = active[1].ID() + cfg.ServerKey = server.Key() + }), + } + expectedIDs := []int{tribes[1].ID()} + slices.SortFunc(tribes, func(a, b domain.Tribe) int { + if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 { + return res + } + return cmp.Compare(a.ID(), b.ID()) + }) + + assert.Equal(t, expectedIDs, tribes.Delete(active)) +} + func TestNewCreateTribeParams(t *testing.T) { t.Parallel() @@ -26,8 +65,21 @@ func TestNewCreateTribeParams(t *testing.T) { domaintest.NewBaseTribe(t), domaintest.NewBaseTribe(t), } + slices.SortFunc(tribes, func(a, b domain.BaseTribe) int { + return cmp.Compare(a.ID(), b.ID()) + }) storedTribes := domain.Tribes{ + domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) { + cfg.ID = tribes[0].ID() + cfg.ServerKey = server.Key()[:len(server.Key())-1] + cfg.BestRank = tribes[0].Rank() + 1 + cfg.BestRankAt = now.Add(-time.Hour) + cfg.MostPoints = tribes[0].AllPoints() - 1 + cfg.MostPointsAt = now.Add(-time.Hour) + cfg.MostVillages = tribes[0].NumVillages() - 1 + cfg.MostVillagesAt = now.Add(-time.Hour) + }), domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) { cfg.ID = tribes[0].ID() cfg.ServerKey = server.Key() @@ -49,6 +101,12 @@ func TestNewCreateTribeParams(t *testing.T) { cfg.MostVillagesAt = now.Add(-time.Hour) }), } + slices.SortFunc(storedTribes, func(a, b domain.Tribe) int { + if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 { + return res + } + return cmp.Compare(a.ID(), b.ID()) + }) expectedParams := []struct { base domain.BaseTribe @@ -70,12 +128,12 @@ func TestNewCreateTribeParams(t *testing.T) { }, { base: tribes[1], - bestRank: storedTribes[1].BestRank(), - bestRankAt: storedTribes[1].BestRankAt(), - mostPoints: storedTribes[1].MostPoints(), - mostPointsAt: storedTribes[1].MostPointsAt(), - mostVillages: storedTribes[1].MostVillages(), - mostVillagesAt: storedTribes[1].MostVillagesAt(), + bestRank: storedTribes[2].BestRank(), + bestRankAt: storedTribes[2].BestRankAt(), + mostPoints: storedTribes[2].MostPoints(), + mostPointsAt: storedTribes[2].MostPointsAt(), + mostVillages: storedTribes[2].MostVillages(), + mostVillagesAt: storedTribes[2].MostVillagesAt(), }, { base: tribes[2],