olivetin/service/internal/executor/group_concurrency_test.go

319 lines
7.4 KiB
Go

package executor
import (
"sync"
"testing"
"time"
"github.com/OliveTin/OliveTin/internal/auth"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testGroupExecutor(actions []*config.Action, groups map[string]*config.ActionGroup) (*Executor, *config.Config) {
cfg := config.DefaultConfig()
cfg.ActionGroups = groups
cfg.Actions = actions
cfg.Sanitize()
e := DefaultExecutor(cfg)
e.RebuildActionMap()
return e, cfg
}
func TestGroupConcurrencyQueuesSecondAction(t *testing.T) {
t.Parallel()
slowAction := &config.Action{
Title: "Unity Job 1",
Shell: "sleep 2",
Groups: []string{"unity"},
}
fastAction := &config.Action{
Title: "Unity Job 2",
Shell: "echo queued-run",
Groups: []string{"unity"},
}
e, cfg := testGroupExecutor(
[]*config.Action{slowAction, fastAction},
map[string]*config.ActionGroup{
"unity": {MaxConcurrent: 1},
},
)
binding1 := e.FindBindingWithNoEntity(slowAction)
binding2 := e.FindBindingWithNoEntity(fastAction)
require.NotNil(t, binding1)
require.NotNil(t, binding2)
wg1, tracking1 := e.ExecRequest(&ExecutionRequest{
Binding: binding1,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
waitUntilExecutionStarted(t, e, tracking1)
wg2, tracking2 := e.ExecRequest(&ExecutionRequest{
Binding: binding2,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
require.Eventually(t, func() bool {
snapshot, ok := e.SnapshotLog(tracking2)
return ok && snapshot.Queued
}, time.Second, 10*time.Millisecond)
wg1.Wait()
wg2.Wait()
snapshot, ok := e.SnapshotLog(tracking2)
require.True(t, ok)
assert.False(t, snapshot.Queued)
assert.False(t, snapshot.Blocked)
assert.Equal(t, int32(0), snapshot.ExitCode)
assert.Contains(t, snapshot.Output, "queued-run")
}
func TestDifferentGroupsRunConcurrently(t *testing.T) {
t.Parallel()
actionA := &config.Action{
Title: "Group A Job",
Shell: "sleep 1",
Groups: []string{"groupA"},
}
actionB := &config.Action{
Title: "Group B Job",
Shell: "echo group-b",
Groups: []string{"groupB"},
}
e, cfg := testGroupExecutor(
[]*config.Action{actionA, actionB},
map[string]*config.ActionGroup{
"groupA": {MaxConcurrent: 1},
"groupB": {MaxConcurrent: 1},
},
)
wg1, tracking1 := e.ExecRequest(&ExecutionRequest{
Binding: e.FindBindingWithNoEntity(actionA),
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
waitUntilExecutionStarted(t, e, tracking1)
wg2, tracking2 := e.ExecRequest(&ExecutionRequest{
Binding: e.FindBindingWithNoEntity(actionB),
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
require.Eventually(t, func() bool {
snapshot, ok := e.SnapshotLog(tracking2)
return ok && snapshot.ExecutionFinished && !snapshot.Queued
}, 2*time.Second, 20*time.Millisecond)
wg1.Wait()
wg2.Wait()
snapshot, ok := e.SnapshotLog(tracking2)
require.True(t, ok)
assert.Contains(t, snapshot.Output, "group-b")
}
func TestPerActionConcurrencyStillBlocksWithoutQueue(t *testing.T) {
t.Parallel()
action := &config.Action{
Title: "Single binding",
Shell: "sleep 1",
MaxConcurrent: 1,
}
e, cfg := testGroupExecutor([]*config.Action{action}, nil)
binding := e.FindBindingWithNoEntity(action)
wg1, tracking1 := e.ExecRequest(&ExecutionRequest{
Binding: binding,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
waitUntilExecutionStarted(t, e, tracking1)
wg2, tracking2 := e.ExecRequest(&ExecutionRequest{
Binding: binding,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
wg1.Wait()
wg2.Wait()
snapshot, ok := e.SnapshotLog(tracking2)
require.True(t, ok)
assert.True(t, snapshot.Blocked)
assert.False(t, snapshot.Queued)
}
func TestPerActionConcurrencyBlocksSameBindingBeforeGroupQueue(t *testing.T) {
t.Parallel()
action := &config.Action{
Title: "Single binding grouped",
Shell: "sleep 1",
MaxConcurrent: 1,
Groups: []string{"unity"},
}
e, cfg := testGroupExecutor(
[]*config.Action{action},
map[string]*config.ActionGroup{
"unity": {MaxConcurrent: 1},
},
)
binding := e.FindBindingWithNoEntity(action)
wg1, tracking1 := e.ExecRequest(&ExecutionRequest{
Binding: binding,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
waitUntilExecutionStarted(t, e, tracking1)
wg2, tracking2 := e.ExecRequest(&ExecutionRequest{
Binding: binding,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
wg1.Wait()
wg2.Wait()
snapshot, ok := e.SnapshotLog(tracking2)
require.True(t, ok)
assert.True(t, snapshot.Blocked)
assert.False(t, snapshot.Queued)
}
func waitUntilExecutionStarted(t *testing.T, e *Executor, trackingID string) {
t.Helper()
require.Eventually(t, func() bool {
snapshot, ok := e.SnapshotLog(trackingID)
return ok && snapshot.ExecutionStarted
}, 2*time.Second, 10*time.Millisecond)
}
func assertWaitGroupPending(t *testing.T, wg *sync.WaitGroup) {
t.Helper()
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
t.Fatal("wait group completed before queued execution finished")
case <-time.After(100 * time.Millisecond):
}
}
func assertWaitGroupCompletes(t *testing.T, wg *sync.WaitGroup) {
t.Helper()
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("wait group did not complete after queue drained")
}
}
func TestStartActionAndWaitWaitsForQueuedExecution(t *testing.T) {
t.Parallel()
first := &config.Action{
Title: "Hold group",
Shell: "sleep 1",
Groups: []string{"unity"},
}
second := &config.Action{
Title: "Wait in queue",
Shell: "echo waited",
Groups: []string{"unity"},
}
e, cfg := testGroupExecutor(
[]*config.Action{first, second},
map[string]*config.ActionGroup{
"unity": {MaxConcurrent: 1},
},
)
wg1, tracking1 := e.ExecRequest(&ExecutionRequest{
Binding: e.FindBindingWithNoEntity(first),
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
waitUntilExecutionStarted(t, e, tracking1)
wg2, tracking2 := e.ExecRequest(&ExecutionRequest{
Binding: e.FindBindingWithNoEntity(second),
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
assertWaitGroupPending(t, wg2)
wg1.Wait()
assertWaitGroupCompletes(t, wg2)
snapshot, ok := e.SnapshotLog(tracking2)
require.True(t, ok)
assert.Contains(t, snapshot.Output, "waited")
}
func TestUnknownActionGroupReferenceWarnsAndSkipsLimit(t *testing.T) {
t.Parallel()
action := &config.Action{
Title: "Unknown group action",
Shell: "echo ok",
Groups: []string{"missing"},
}
e, cfg := testGroupExecutor([]*config.Action{action}, map[string]*config.ActionGroup{})
wg, tracking := e.ExecRequest(&ExecutionRequest{
Binding: e.FindBindingWithNoEntity(action),
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
wg.Wait()
snapshot, ok := e.SnapshotLog(tracking)
require.True(t, ok)
assert.False(t, snapshot.Queued)
assert.Equal(t, int32(0), snapshot.ExitCode)
}