group_capacity_service.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. package service
  2. import (
  3. "context"
  4. "time"
  5. )
  6. // GroupCapacitySummary holds aggregated capacity for a single group.
  7. type GroupCapacitySummary struct {
  8. GroupID int64 `json:"group_id"`
  9. ConcurrencyUsed int `json:"concurrency_used"`
  10. ConcurrencyMax int `json:"concurrency_max"`
  11. SessionsUsed int `json:"sessions_used"`
  12. SessionsMax int `json:"sessions_max"`
  13. RPMUsed int `json:"rpm_used"`
  14. RPMMax int `json:"rpm_max"`
  15. }
  16. // GroupAccountCapacityRow is the lightweight account projection needed for
  17. // capacity summary aggregation.
  18. type GroupAccountCapacityRow struct {
  19. GroupID int64
  20. AccountID int64
  21. Concurrency int
  22. Extra map[string]any
  23. SessionWindowStart *time.Time
  24. SessionWindowEnd *time.Time
  25. SessionWindowStatus string
  26. }
  27. type groupCapacityActiveGroupIDLister interface {
  28. ListActiveIDs(ctx context.Context) ([]int64, error)
  29. }
  30. type groupCapacityAccountLister interface {
  31. ListSchedulableCapacityByGroupIDs(ctx context.Context, groupIDs []int64) ([]GroupAccountCapacityRow, error)
  32. }
  33. // GroupCapacityService aggregates per-group capacity from runtime data.
  34. type GroupCapacityService struct {
  35. accountRepo AccountRepository
  36. groupRepo GroupRepository
  37. concurrencyService *ConcurrencyService
  38. sessionLimitCache SessionLimitCache
  39. rpmCache RPMCache
  40. }
  41. // NewGroupCapacityService creates a new GroupCapacityService.
  42. func NewGroupCapacityService(
  43. accountRepo AccountRepository,
  44. groupRepo GroupRepository,
  45. concurrencyService *ConcurrencyService,
  46. sessionLimitCache SessionLimitCache,
  47. rpmCache RPMCache,
  48. ) *GroupCapacityService {
  49. return &GroupCapacityService{
  50. accountRepo: accountRepo,
  51. groupRepo: groupRepo,
  52. concurrencyService: concurrencyService,
  53. sessionLimitCache: sessionLimitCache,
  54. rpmCache: rpmCache,
  55. }
  56. }
  57. // GetAllGroupCapacity returns capacity summary for all active groups.
  58. func (s *GroupCapacityService) GetAllGroupCapacity(ctx context.Context) ([]GroupCapacitySummary, error) {
  59. groupIDs, err := s.listActiveGroupIDs(ctx)
  60. if err != nil {
  61. return nil, err
  62. }
  63. if lister, ok := s.accountRepo.(groupCapacityAccountLister); ok {
  64. return s.getGroupCapacitiesBatch(ctx, groupIDs, lister)
  65. }
  66. return s.getGroupCapacitiesSequential(ctx, groupIDs), nil
  67. }
  68. func (s *GroupCapacityService) listActiveGroupIDs(ctx context.Context) ([]int64, error) {
  69. if lister, ok := s.groupRepo.(groupCapacityActiveGroupIDLister); ok {
  70. return lister.ListActiveIDs(ctx)
  71. }
  72. groups, err := s.groupRepo.ListActive(ctx)
  73. if err != nil {
  74. return nil, err
  75. }
  76. groupIDs := make([]int64, 0, len(groups))
  77. for i := range groups {
  78. groupIDs = append(groupIDs, groups[i].ID)
  79. }
  80. return groupIDs, nil
  81. }
  82. func (s *GroupCapacityService) getGroupCapacitiesSequential(ctx context.Context, groupIDs []int64) []GroupCapacitySummary {
  83. results := make([]GroupCapacitySummary, 0, len(groupIDs))
  84. for _, groupID := range groupIDs {
  85. cap, err := s.getGroupCapacity(ctx, groupID)
  86. if err != nil {
  87. // Skip groups with errors, return partial results
  88. continue
  89. }
  90. cap.GroupID = groupID
  91. results = append(results, cap)
  92. }
  93. return results
  94. }
  95. type groupCapacityAccountRef struct {
  96. groupID int64
  97. accountID int64
  98. }
  99. func (s *GroupCapacityService) getGroupCapacitiesBatch(ctx context.Context, groupIDs []int64, lister groupCapacityAccountLister) ([]GroupCapacitySummary, error) {
  100. results := make([]GroupCapacitySummary, len(groupIDs))
  101. groupIndex := make(map[int64]int, len(groupIDs))
  102. for i, groupID := range groupIDs {
  103. results[i].GroupID = groupID
  104. groupIndex[groupID] = i
  105. }
  106. if len(groupIDs) == 0 {
  107. return results, nil
  108. }
  109. rows, err := lister.ListSchedulableCapacityByGroupIDs(ctx, groupIDs)
  110. if err != nil {
  111. return nil, err
  112. }
  113. if len(rows) == 0 {
  114. return results, nil
  115. }
  116. refs := make([]groupCapacityAccountRef, 0, len(rows))
  117. seenGroupAccount := make(map[groupCapacityAccountRef]struct{}, len(rows))
  118. accountIDSet := make(map[int64]struct{}, len(rows))
  119. accountIDs := make([]int64, 0, len(rows))
  120. sessionTimeouts := make(map[int64]time.Duration)
  121. for _, row := range rows {
  122. idx, ok := groupIndex[row.GroupID]
  123. if !ok || row.AccountID <= 0 {
  124. continue
  125. }
  126. ref := groupCapacityAccountRef{groupID: row.GroupID, accountID: row.AccountID}
  127. if _, ok := seenGroupAccount[ref]; ok {
  128. continue
  129. }
  130. seenGroupAccount[ref] = struct{}{}
  131. refs = append(refs, ref)
  132. if _, ok := accountIDSet[row.AccountID]; !ok {
  133. accountIDSet[row.AccountID] = struct{}{}
  134. accountIDs = append(accountIDs, row.AccountID)
  135. }
  136. acc := Account{
  137. ID: row.AccountID,
  138. Concurrency: row.Concurrency,
  139. Extra: row.Extra,
  140. SessionWindowStart: row.SessionWindowStart,
  141. SessionWindowEnd: row.SessionWindowEnd,
  142. SessionWindowStatus: row.SessionWindowStatus,
  143. }
  144. results[idx].ConcurrencyMax += acc.Concurrency
  145. if maxSessions := acc.GetMaxSessions(); maxSessions > 0 {
  146. results[idx].SessionsMax += maxSessions
  147. timeout := time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute
  148. if timeout <= 0 {
  149. timeout = 5 * time.Minute
  150. }
  151. sessionTimeouts[acc.ID] = timeout
  152. }
  153. if rpm := acc.GetBaseRPM(); rpm > 0 {
  154. results[idx].RPMMax += rpm
  155. }
  156. }
  157. if len(accountIDs) == 0 {
  158. return results, nil
  159. }
  160. concurrencyMap := map[int64]int{}
  161. if s.concurrencyService != nil {
  162. concurrencyMap, _ = s.concurrencyService.GetAccountConcurrencyBatch(ctx, accountIDs)
  163. }
  164. sessionAccountIDs := accountIDsForGroupsWithLimit(refs, groupIndex, results, func(summary GroupCapacitySummary) bool {
  165. return summary.SessionsMax > 0
  166. })
  167. var sessionsMap map[int64]int
  168. if len(sessionAccountIDs) > 0 && s.sessionLimitCache != nil {
  169. sessionsMap, _ = s.sessionLimitCache.GetActiveSessionCountBatch(ctx, sessionAccountIDs, sessionTimeouts)
  170. }
  171. rpmAccountIDs := accountIDsForGroupsWithLimit(refs, groupIndex, results, func(summary GroupCapacitySummary) bool {
  172. return summary.RPMMax > 0
  173. })
  174. var rpmMap map[int64]int
  175. if len(rpmAccountIDs) > 0 && s.rpmCache != nil {
  176. rpmMap, _ = s.rpmCache.GetRPMBatch(ctx, rpmAccountIDs)
  177. }
  178. for _, ref := range refs {
  179. idx := groupIndex[ref.groupID]
  180. results[idx].ConcurrencyUsed += concurrencyMap[ref.accountID]
  181. if sessionsMap != nil && results[idx].SessionsMax > 0 {
  182. results[idx].SessionsUsed += sessionsMap[ref.accountID]
  183. }
  184. if rpmMap != nil && results[idx].RPMMax > 0 {
  185. results[idx].RPMUsed += rpmMap[ref.accountID]
  186. }
  187. }
  188. return results, nil
  189. }
  190. func accountIDsForGroupsWithLimit(refs []groupCapacityAccountRef, groupIndex map[int64]int, summaries []GroupCapacitySummary, include func(GroupCapacitySummary) bool) []int64 {
  191. seen := make(map[int64]struct{})
  192. accountIDs := make([]int64, 0)
  193. for _, ref := range refs {
  194. idx, ok := groupIndex[ref.groupID]
  195. if !ok || !include(summaries[idx]) {
  196. continue
  197. }
  198. if _, ok := seen[ref.accountID]; ok {
  199. continue
  200. }
  201. seen[ref.accountID] = struct{}{}
  202. accountIDs = append(accountIDs, ref.accountID)
  203. }
  204. return accountIDs
  205. }
  206. func (s *GroupCapacityService) getGroupCapacity(ctx context.Context, groupID int64) (GroupCapacitySummary, error) {
  207. accounts, err := s.accountRepo.ListSchedulableByGroupID(ctx, groupID)
  208. if err != nil {
  209. return GroupCapacitySummary{}, err
  210. }
  211. if len(accounts) == 0 {
  212. return GroupCapacitySummary{}, nil
  213. }
  214. // Collect account IDs and config values
  215. accountIDs := make([]int64, 0, len(accounts))
  216. sessionTimeouts := make(map[int64]time.Duration)
  217. var concurrencyMax, sessionsMax, rpmMax int
  218. for i := range accounts {
  219. acc := &accounts[i]
  220. accountIDs = append(accountIDs, acc.ID)
  221. concurrencyMax += acc.Concurrency
  222. if ms := acc.GetMaxSessions(); ms > 0 {
  223. sessionsMax += ms
  224. timeout := time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute
  225. if timeout <= 0 {
  226. timeout = 5 * time.Minute
  227. }
  228. sessionTimeouts[acc.ID] = timeout
  229. }
  230. if rpm := acc.GetBaseRPM(); rpm > 0 {
  231. rpmMax += rpm
  232. }
  233. }
  234. // Batch query runtime data from Redis
  235. concurrencyMap, _ := s.concurrencyService.GetAccountConcurrencyBatch(ctx, accountIDs)
  236. var sessionsMap map[int64]int
  237. if sessionsMax > 0 && s.sessionLimitCache != nil {
  238. sessionsMap, _ = s.sessionLimitCache.GetActiveSessionCountBatch(ctx, accountIDs, sessionTimeouts)
  239. }
  240. var rpmMap map[int64]int
  241. if rpmMax > 0 && s.rpmCache != nil {
  242. rpmMap, _ = s.rpmCache.GetRPMBatch(ctx, accountIDs)
  243. }
  244. // Aggregate
  245. var concurrencyUsed, sessionsUsed, rpmUsed int
  246. for _, id := range accountIDs {
  247. concurrencyUsed += concurrencyMap[id]
  248. if sessionsMap != nil {
  249. sessionsUsed += sessionsMap[id]
  250. }
  251. if rpmMap != nil {
  252. rpmUsed += rpmMap[id]
  253. }
  254. }
  255. return GroupCapacitySummary{
  256. ConcurrencyUsed: concurrencyUsed,
  257. ConcurrencyMax: concurrencyMax,
  258. SessionsUsed: sessionsUsed,
  259. SessionsMax: sessionsMax,
  260. RPMUsed: rpmUsed,
  261. RPMMax: rpmMax,
  262. }, nil
  263. }