group_concurrency.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package executor
  2. import (
  3. "fmt"
  4. "slices"
  5. "sync"
  6. config "github.com/OliveTin/OliveTin/internal/config"
  7. log "github.com/sirupsen/logrus"
  8. )
  9. type groupLimit struct {
  10. name string
  11. maxConcurrent int
  12. }
  13. type queuedExecution struct {
  14. req *ExecutionRequest
  15. wg *sync.WaitGroup
  16. }
  17. func actionGroupLimits(req *ExecutionRequest) []groupLimit {
  18. if !hasActionGroupContext(req) {
  19. return nil
  20. }
  21. limits := make([]groupLimit, 0, len(req.Binding.Action.Groups))
  22. for _, groupName := range req.Binding.Action.Groups {
  23. if limit, ok := groupLimitFromConfig(req.Cfg, groupName); ok {
  24. limits = append(limits, limit)
  25. }
  26. }
  27. return limits
  28. }
  29. func hasActionGroupContext(req *ExecutionRequest) bool {
  30. return req != nil && req.Binding != nil && req.Binding.Action != nil && req.Cfg != nil
  31. }
  32. func groupLimitFromConfig(cfg *config.Config, groupName string) (groupLimit, bool) {
  33. group, found := cfg.ActionGroups[groupName]
  34. if !found || group == nil || group.MaxConcurrent < 1 {
  35. return groupLimit{}, false
  36. }
  37. return groupLimit{name: groupName, maxConcurrent: group.MaxConcurrent}, true
  38. }
  39. func actionNeedsGroupLimit(req *ExecutionRequest) bool {
  40. return len(actionGroupLimits(req)) > 0
  41. }
  42. func actionInGroup(action *config.Action, groupName string) bool {
  43. if action == nil {
  44. return false
  45. }
  46. return slices.Contains(action.Groups, groupName)
  47. }
  48. func (e *Executor) countActiveInGroup(groupName string) int {
  49. count := 0
  50. e.logmutex.RLock()
  51. defer e.logmutex.RUnlock()
  52. for _, logEntry := range e.logs {
  53. if logEntryIsActiveInGroup(logEntry, groupName) {
  54. count++
  55. }
  56. }
  57. return count
  58. }
  59. func logEntryIsActiveInGroup(logEntry *InternalLogEntry, groupName string) bool {
  60. if inactiveLogEntry(logEntry) {
  61. return false
  62. }
  63. return actionInGroup(logEntry.Binding.Action, groupName)
  64. }
  65. func inactiveLogEntry(logEntry *InternalLogEntry) bool {
  66. if logEntry == nil {
  67. return true
  68. }
  69. return logEntryIsInactive(logEntry)
  70. }
  71. func logEntryIsInactive(logEntry *InternalLogEntry) bool {
  72. if logEntry.ExecutionFinished || logEntry.Queued {
  73. return true
  74. }
  75. return logEntry.Binding == nil || logEntry.Binding.Action == nil
  76. }
  77. func (e *Executor) groupsHaveCapacityForActive(req *ExecutionRequest) bool {
  78. for _, limit := range actionGroupLimits(req) {
  79. if e.countActiveInGroup(limit.name) >= (limit.maxConcurrent + 1) {
  80. return false
  81. }
  82. }
  83. return true
  84. }
  85. func (e *Executor) groupsHaveCapacityForQueued(req *ExecutionRequest) bool {
  86. for _, limit := range actionGroupLimits(req) {
  87. if e.countActiveInGroup(limit.name) >= limit.maxConcurrent {
  88. return false
  89. }
  90. }
  91. return true
  92. }
  93. func firstFullGroupName(e *Executor, req *ExecutionRequest) string {
  94. for _, limit := range actionGroupLimits(req) {
  95. if e.countActiveInGroup(limit.name) >= (limit.maxConcurrent + 1) {
  96. return limit.name
  97. }
  98. }
  99. return ""
  100. }
  101. func (e *Executor) queueRequest(req *ExecutionRequest, wg *sync.WaitGroup) {
  102. groupName := firstFullGroupName(e, req)
  103. req.logEntry.Queued = true
  104. req.logEntry.QueuedForGroup = groupName
  105. req.logEntry.Output = fmt.Sprintf("Queued waiting for action group %q", groupName)
  106. log.WithFields(log.Fields{
  107. "actionTitle": req.logEntry.ActionTitle,
  108. "groupName": groupName,
  109. }).Infof("Action queued due to action group concurrency limit")
  110. e.groupQueueMu.Lock()
  111. e.groupQueue = append(e.groupQueue, &queuedExecution{req: req, wg: wg})
  112. e.groupQueueMu.Unlock()
  113. }
  114. func (e *Executor) drainGroupQueue() {
  115. e.groupQueueMu.Lock()
  116. if len(e.groupQueue) == 0 {
  117. e.groupQueueMu.Unlock()
  118. return
  119. }
  120. next := e.groupQueue[0]
  121. if !e.groupsHaveCapacityForQueued(next.req) {
  122. e.groupQueueMu.Unlock()
  123. return
  124. }
  125. e.groupQueue = e.groupQueue[1:]
  126. e.groupQueueMu.Unlock()
  127. go e.runDequeuedExecution(next)
  128. }
  129. func (e *Executor) runDequeuedExecution(queued *queuedExecution) {
  130. req := queued.req
  131. e.logmutex.Lock()
  132. req.logEntry.Queued = false
  133. req.logEntry.QueuedForGroup = ""
  134. e.logmutex.Unlock()
  135. req.skipRequestRegistration = true
  136. e.runExecutionSteps(req)
  137. e.finishExecChain(req)
  138. queued.wg.Done()
  139. }