Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
304 lines
8.7 KiB
Go
304 lines
8.7 KiB
Go
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
|
|
}
|