| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- package service
- import (
- "context"
- "time"
- )
- // GroupCapacitySummary holds aggregated capacity for a single group.
- type GroupCapacitySummary struct {
- GroupID int64 `json:"group_id"`
- ConcurrencyUsed int `json:"concurrency_used"`
- ConcurrencyMax int `json:"concurrency_max"`
- SessionsUsed int `json:"sessions_used"`
- SessionsMax int `json:"sessions_max"`
- RPMUsed int `json:"rpm_used"`
- RPMMax int `json:"rpm_max"`
- }
- // GroupAccountCapacityRow is the lightweight account projection needed for
- // capacity summary aggregation.
- type GroupAccountCapacityRow struct {
- GroupID int64
- AccountID int64
- Concurrency int
- Extra map[string]any
- SessionWindowStart *time.Time
- SessionWindowEnd *time.Time
- SessionWindowStatus string
- }
- type groupCapacityActiveGroupIDLister interface {
- ListActiveIDs(ctx context.Context) ([]int64, error)
- }
- type groupCapacityAccountLister interface {
- ListSchedulableCapacityByGroupIDs(ctx context.Context, groupIDs []int64) ([]GroupAccountCapacityRow, error)
- }
- // GroupCapacityService aggregates per-group capacity from runtime data.
- type GroupCapacityService struct {
- accountRepo AccountRepository
- groupRepo GroupRepository
- concurrencyService *ConcurrencyService
- sessionLimitCache SessionLimitCache
- rpmCache RPMCache
- }
- // NewGroupCapacityService creates a new GroupCapacityService.
- func NewGroupCapacityService(
- accountRepo AccountRepository,
- groupRepo GroupRepository,
- concurrencyService *ConcurrencyService,
- sessionLimitCache SessionLimitCache,
- rpmCache RPMCache,
- ) *GroupCapacityService {
- return &GroupCapacityService{
- accountRepo: accountRepo,
- groupRepo: groupRepo,
- concurrencyService: concurrencyService,
- sessionLimitCache: sessionLimitCache,
- rpmCache: rpmCache,
- }
- }
- // GetAllGroupCapacity returns capacity summary for all active groups.
- func (s *GroupCapacityService) GetAllGroupCapacity(ctx context.Context) ([]GroupCapacitySummary, error) {
- groupIDs, err := s.listActiveGroupIDs(ctx)
- if err != nil {
- return nil, err
- }
- if lister, ok := s.accountRepo.(groupCapacityAccountLister); ok {
- return s.getGroupCapacitiesBatch(ctx, groupIDs, lister)
- }
- return s.getGroupCapacitiesSequential(ctx, groupIDs), nil
- }
- func (s *GroupCapacityService) listActiveGroupIDs(ctx context.Context) ([]int64, error) {
- if lister, ok := s.groupRepo.(groupCapacityActiveGroupIDLister); ok {
- return lister.ListActiveIDs(ctx)
- }
- groups, err := s.groupRepo.ListActive(ctx)
- if err != nil {
- return nil, err
- }
- groupIDs := make([]int64, 0, len(groups))
- for i := range groups {
- groupIDs = append(groupIDs, groups[i].ID)
- }
- return groupIDs, nil
- }
- func (s *GroupCapacityService) getGroupCapacitiesSequential(ctx context.Context, groupIDs []int64) []GroupCapacitySummary {
- results := make([]GroupCapacitySummary, 0, len(groupIDs))
- for _, groupID := range groupIDs {
- cap, err := s.getGroupCapacity(ctx, groupID)
- if err != nil {
- // Skip groups with errors, return partial results
- continue
- }
- cap.GroupID = groupID
- results = append(results, cap)
- }
- return results
- }
- type groupCapacityAccountRef struct {
- groupID int64
- accountID int64
- }
- func (s *GroupCapacityService) getGroupCapacitiesBatch(ctx context.Context, groupIDs []int64, lister groupCapacityAccountLister) ([]GroupCapacitySummary, error) {
- results := make([]GroupCapacitySummary, len(groupIDs))
- groupIndex := make(map[int64]int, len(groupIDs))
- for i, groupID := range groupIDs {
- results[i].GroupID = groupID
- groupIndex[groupID] = i
- }
- if len(groupIDs) == 0 {
- return results, nil
- }
- rows, err := lister.ListSchedulableCapacityByGroupIDs(ctx, groupIDs)
- if err != nil {
- return nil, err
- }
- if len(rows) == 0 {
- return results, nil
- }
- refs := make([]groupCapacityAccountRef, 0, len(rows))
- seenGroupAccount := make(map[groupCapacityAccountRef]struct{}, len(rows))
- accountIDSet := make(map[int64]struct{}, len(rows))
- accountIDs := make([]int64, 0, len(rows))
- sessionTimeouts := make(map[int64]time.Duration)
- for _, row := range rows {
- idx, ok := groupIndex[row.GroupID]
- if !ok || row.AccountID <= 0 {
- continue
- }
- ref := groupCapacityAccountRef{groupID: row.GroupID, accountID: row.AccountID}
- if _, ok := seenGroupAccount[ref]; ok {
- continue
- }
- seenGroupAccount[ref] = struct{}{}
- refs = append(refs, ref)
- if _, ok := accountIDSet[row.AccountID]; !ok {
- accountIDSet[row.AccountID] = struct{}{}
- accountIDs = append(accountIDs, row.AccountID)
- }
- acc := Account{
- ID: row.AccountID,
- Concurrency: row.Concurrency,
- Extra: row.Extra,
- SessionWindowStart: row.SessionWindowStart,
- SessionWindowEnd: row.SessionWindowEnd,
- SessionWindowStatus: row.SessionWindowStatus,
- }
- results[idx].ConcurrencyMax += acc.Concurrency
- if maxSessions := acc.GetMaxSessions(); maxSessions > 0 {
- results[idx].SessionsMax += maxSessions
- timeout := time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute
- if timeout <= 0 {
- timeout = 5 * time.Minute
- }
- sessionTimeouts[acc.ID] = timeout
- }
- if rpm := acc.GetBaseRPM(); rpm > 0 {
- results[idx].RPMMax += rpm
- }
- }
- if len(accountIDs) == 0 {
- return results, nil
- }
- concurrencyMap := map[int64]int{}
- if s.concurrencyService != nil {
- concurrencyMap, _ = s.concurrencyService.GetAccountConcurrencyBatch(ctx, accountIDs)
- }
- sessionAccountIDs := accountIDsForGroupsWithLimit(refs, groupIndex, results, func(summary GroupCapacitySummary) bool {
- return summary.SessionsMax > 0
- })
- var sessionsMap map[int64]int
- if len(sessionAccountIDs) > 0 && s.sessionLimitCache != nil {
- sessionsMap, _ = s.sessionLimitCache.GetActiveSessionCountBatch(ctx, sessionAccountIDs, sessionTimeouts)
- }
- rpmAccountIDs := accountIDsForGroupsWithLimit(refs, groupIndex, results, func(summary GroupCapacitySummary) bool {
- return summary.RPMMax > 0
- })
- var rpmMap map[int64]int
- if len(rpmAccountIDs) > 0 && s.rpmCache != nil {
- rpmMap, _ = s.rpmCache.GetRPMBatch(ctx, rpmAccountIDs)
- }
- for _, ref := range refs {
- idx := groupIndex[ref.groupID]
- results[idx].ConcurrencyUsed += concurrencyMap[ref.accountID]
- if sessionsMap != nil && results[idx].SessionsMax > 0 {
- results[idx].SessionsUsed += sessionsMap[ref.accountID]
- }
- if rpmMap != nil && results[idx].RPMMax > 0 {
- results[idx].RPMUsed += rpmMap[ref.accountID]
- }
- }
- return results, nil
- }
- func accountIDsForGroupsWithLimit(refs []groupCapacityAccountRef, groupIndex map[int64]int, summaries []GroupCapacitySummary, include func(GroupCapacitySummary) bool) []int64 {
- seen := make(map[int64]struct{})
- accountIDs := make([]int64, 0)
- for _, ref := range refs {
- idx, ok := groupIndex[ref.groupID]
- if !ok || !include(summaries[idx]) {
- continue
- }
- if _, ok := seen[ref.accountID]; ok {
- continue
- }
- seen[ref.accountID] = struct{}{}
- accountIDs = append(accountIDs, ref.accountID)
- }
- return accountIDs
- }
- func (s *GroupCapacityService) getGroupCapacity(ctx context.Context, groupID int64) (GroupCapacitySummary, error) {
- accounts, err := s.accountRepo.ListSchedulableByGroupID(ctx, groupID)
- if err != nil {
- return GroupCapacitySummary{}, err
- }
- if len(accounts) == 0 {
- return GroupCapacitySummary{}, nil
- }
- // Collect account IDs and config values
- accountIDs := make([]int64, 0, len(accounts))
- sessionTimeouts := make(map[int64]time.Duration)
- var concurrencyMax, sessionsMax, rpmMax int
- for i := range accounts {
- acc := &accounts[i]
- accountIDs = append(accountIDs, acc.ID)
- concurrencyMax += acc.Concurrency
- if ms := acc.GetMaxSessions(); ms > 0 {
- sessionsMax += ms
- timeout := time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute
- if timeout <= 0 {
- timeout = 5 * time.Minute
- }
- sessionTimeouts[acc.ID] = timeout
- }
- if rpm := acc.GetBaseRPM(); rpm > 0 {
- rpmMax += rpm
- }
- }
- // Batch query runtime data from Redis
- concurrencyMap, _ := s.concurrencyService.GetAccountConcurrencyBatch(ctx, accountIDs)
- var sessionsMap map[int64]int
- if sessionsMax > 0 && s.sessionLimitCache != nil {
- sessionsMap, _ = s.sessionLimitCache.GetActiveSessionCountBatch(ctx, accountIDs, sessionTimeouts)
- }
- var rpmMap map[int64]int
- if rpmMax > 0 && s.rpmCache != nil {
- rpmMap, _ = s.rpmCache.GetRPMBatch(ctx, accountIDs)
- }
- // Aggregate
- var concurrencyUsed, sessionsUsed, rpmUsed int
- for _, id := range accountIDs {
- concurrencyUsed += concurrencyMap[id]
- if sessionsMap != nil {
- sessionsUsed += sessionsMap[id]
- }
- if rpmMap != nil {
- rpmUsed += rpmMap[id]
- }
- }
- return GroupCapacitySummary{
- ConcurrencyUsed: concurrencyUsed,
- ConcurrencyMax: concurrencyMax,
- SessionsUsed: sessionsUsed,
- SessionsMax: sessionsMax,
- RPMUsed: rpmUsed,
- RPMMax: rpmMax,
- }, nil
- }
|