200 lines
4.3 KiB
Go
200 lines
4.3 KiB
Go
package executor
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
|
|
config "github.com/OliveTin/OliveTin/internal/config"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type groupLimit struct {
|
|
name string
|
|
maxConcurrent int
|
|
}
|
|
|
|
type queuedExecution struct {
|
|
req *ExecutionRequest
|
|
wg *sync.WaitGroup
|
|
}
|
|
|
|
func actionGroupLimits(req *ExecutionRequest) []groupLimit {
|
|
if !hasActionGroupContext(req) {
|
|
return nil
|
|
}
|
|
|
|
limits := make([]groupLimit, 0, len(req.Binding.Action.Groups))
|
|
|
|
for _, groupName := range req.Binding.Action.Groups {
|
|
if limit, ok := groupLimitFromConfig(req.Cfg, groupName); ok {
|
|
limits = append(limits, limit)
|
|
}
|
|
}
|
|
|
|
return limits
|
|
}
|
|
|
|
func hasActionGroupContext(req *ExecutionRequest) bool {
|
|
return req != nil && req.Binding != nil && req.Binding.Action != nil && req.Cfg != nil
|
|
}
|
|
|
|
func groupLimitFromConfig(cfg *config.Config, groupName string) (groupLimit, bool) {
|
|
group, found := cfg.ActionGroups[groupName]
|
|
if !found || group == nil || group.MaxConcurrent < 1 {
|
|
return groupLimit{}, false
|
|
}
|
|
|
|
return groupLimit{name: groupName, maxConcurrent: group.MaxConcurrent}, true
|
|
}
|
|
|
|
func actionNeedsGroupLimit(req *ExecutionRequest) bool {
|
|
return len(actionGroupLimits(req)) > 0
|
|
}
|
|
|
|
func actionInGroup(action *config.Action, groupName string) bool {
|
|
if action == nil {
|
|
return false
|
|
}
|
|
|
|
return slices.Contains(action.Groups, groupName)
|
|
}
|
|
|
|
func (e *Executor) countActiveInGroup(groupName string) int {
|
|
e.logmutex.RLock()
|
|
defer e.logmutex.RUnlock()
|
|
|
|
return e.countActiveInGroupLocked(groupName)
|
|
}
|
|
|
|
func (e *Executor) countActiveInGroupLocked(groupName string) int {
|
|
count := 0
|
|
|
|
for _, logEntry := range e.logs {
|
|
if logEntryIsActiveInGroup(logEntry, groupName) {
|
|
count++
|
|
}
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
func logEntryIsActiveInGroup(logEntry *InternalLogEntry, groupName string) bool {
|
|
if inactiveLogEntry(logEntry) {
|
|
return false
|
|
}
|
|
|
|
return actionInGroup(logEntry.Binding.Action, groupName)
|
|
}
|
|
|
|
func inactiveLogEntry(logEntry *InternalLogEntry) bool {
|
|
if logEntry == nil {
|
|
return true
|
|
}
|
|
|
|
return logEntryIsInactive(logEntry)
|
|
}
|
|
|
|
func logEntryIsInactive(logEntry *InternalLogEntry) bool {
|
|
if logEntry.ExecutionFinished || logEntry.Queued {
|
|
return true
|
|
}
|
|
|
|
return logEntry.Binding == nil || logEntry.Binding.Action == nil
|
|
}
|
|
|
|
func (e *Executor) groupsHaveCapacityForActive(req *ExecutionRequest) bool {
|
|
for _, limit := range actionGroupLimits(req) {
|
|
if e.countActiveInGroup(limit.name) >= (limit.maxConcurrent + 1) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (e *Executor) groupsHaveCapacityForQueued(req *ExecutionRequest) bool {
|
|
for _, limit := range actionGroupLimits(req) {
|
|
if e.countActiveInGroup(limit.name) >= limit.maxConcurrent {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func firstFullGroupName(e *Executor, req *ExecutionRequest) string {
|
|
for _, limit := range actionGroupLimits(req) {
|
|
if e.countActiveInGroup(limit.name) >= (limit.maxConcurrent + 1) {
|
|
return limit.name
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func firstFullGroupNameLocked(e *Executor, req *ExecutionRequest) string {
|
|
for _, limit := range actionGroupLimits(req) {
|
|
if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) {
|
|
return limit.name
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (e *Executor) queueRequest(req *ExecutionRequest, wg *sync.WaitGroup) {
|
|
var groupName string
|
|
|
|
req.mutateLogEntry(func(entry *InternalLogEntry) {
|
|
groupName = firstFullGroupNameLocked(e, req)
|
|
entry.Queued = true
|
|
entry.QueuedForGroup = groupName
|
|
entry.Output = fmt.Sprintf("Queued waiting for action group %q", groupName)
|
|
})
|
|
|
|
log.WithFields(log.Fields{
|
|
"actionTitle": req.logEntry.ActionTitle,
|
|
"groupName": groupName,
|
|
}).Infof("Action queued due to action group concurrency limit")
|
|
|
|
e.groupQueueMu.Lock()
|
|
e.groupQueue = append(e.groupQueue, &queuedExecution{req: req, wg: wg})
|
|
e.groupQueueMu.Unlock()
|
|
}
|
|
|
|
func (e *Executor) drainGroupQueue() {
|
|
e.groupQueueMu.Lock()
|
|
|
|
if len(e.groupQueue) == 0 {
|
|
e.groupQueueMu.Unlock()
|
|
return
|
|
}
|
|
|
|
next := e.groupQueue[0]
|
|
if !e.groupsHaveCapacityForQueued(next.req) {
|
|
e.groupQueueMu.Unlock()
|
|
return
|
|
}
|
|
|
|
e.groupQueue = e.groupQueue[1:]
|
|
e.groupQueueMu.Unlock()
|
|
|
|
next.req.mutateLogEntry(func(entry *InternalLogEntry) {
|
|
entry.Queued = false
|
|
entry.QueuedForGroup = ""
|
|
})
|
|
|
|
go e.runDequeuedExecution(next)
|
|
}
|
|
|
|
func (e *Executor) runDequeuedExecution(queued *queuedExecution) {
|
|
req := queued.req
|
|
|
|
req.skipRequestRegistration = true
|
|
|
|
e.runExecutionSteps(req)
|
|
e.finishExecChain(req)
|
|
queued.wg.Done()
|
|
}
|