19a2ee71d3
Fixes #6787 In ProposedAllocs the proposed alloc slice was being copied while its contents were not. Since RemoveAllocs nils elements of the proposed alloc slice and is called twice, it could panic on the second call when erroneously accessing a nil'd alloc. The fix is to not copy the proposed alloc slice and pass the slice returned by the 1st RemoveAllocs call to the 2nd call, thus maintaining the trimmed length.
395 lines
10 KiB
Go
395 lines
10 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func testContext(t testing.TB) (*state.StateStore, *EvalContext) {
|
|
state := state.TestStateStore(t)
|
|
plan := &structs.Plan{
|
|
NodeUpdate: make(map[string][]*structs.Allocation),
|
|
NodeAllocation: make(map[string][]*structs.Allocation),
|
|
NodePreemptions: make(map[string][]*structs.Allocation),
|
|
}
|
|
|
|
logger := testlog.HCLogger(t)
|
|
|
|
ctx := NewEvalContext(state, plan, logger)
|
|
return state, ctx
|
|
}
|
|
|
|
func TestEvalContext_ProposedAlloc(t *testing.T) {
|
|
state, ctx := testContext(t)
|
|
nodes := []*RankedNode{
|
|
{
|
|
Node: &structs.Node{
|
|
// Perfect fit
|
|
ID: uuid.Generate(),
|
|
NodeResources: &structs.NodeResources{
|
|
Cpu: structs.NodeCpuResources{
|
|
CpuShares: 2048,
|
|
},
|
|
Memory: structs.NodeMemoryResources{
|
|
MemoryMB: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Node: &structs.Node{
|
|
// Perfect fit
|
|
ID: uuid.Generate(),
|
|
NodeResources: &structs.NodeResources{
|
|
Cpu: structs.NodeCpuResources{
|
|
CpuShares: 2048,
|
|
},
|
|
Memory: structs.NodeMemoryResources{
|
|
MemoryMB: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Add existing allocations
|
|
j1, j2 := mock.Job(), mock.Job()
|
|
alloc1 := &structs.Allocation{
|
|
ID: uuid.Generate(),
|
|
Namespace: structs.DefaultNamespace,
|
|
EvalID: uuid.Generate(),
|
|
NodeID: nodes[0].Node.ID,
|
|
JobID: j1.ID,
|
|
Job: j1,
|
|
AllocatedResources: &structs.AllocatedResources{
|
|
Tasks: map[string]*structs.AllocatedTaskResources{
|
|
"web": {
|
|
Cpu: structs.AllocatedCpuResources{
|
|
CpuShares: 2048,
|
|
},
|
|
Memory: structs.AllocatedMemoryResources{
|
|
MemoryMB: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DesiredStatus: structs.AllocDesiredStatusRun,
|
|
ClientStatus: structs.AllocClientStatusPending,
|
|
TaskGroup: "web",
|
|
}
|
|
alloc2 := &structs.Allocation{
|
|
ID: uuid.Generate(),
|
|
Namespace: structs.DefaultNamespace,
|
|
EvalID: uuid.Generate(),
|
|
NodeID: nodes[1].Node.ID,
|
|
JobID: j2.ID,
|
|
Job: j2,
|
|
AllocatedResources: &structs.AllocatedResources{
|
|
Tasks: map[string]*structs.AllocatedTaskResources{
|
|
"web": {
|
|
Cpu: structs.AllocatedCpuResources{
|
|
CpuShares: 1024,
|
|
},
|
|
Memory: structs.AllocatedMemoryResources{
|
|
MemoryMB: 1024,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DesiredStatus: structs.AllocDesiredStatusRun,
|
|
ClientStatus: structs.AllocClientStatusPending,
|
|
TaskGroup: "web",
|
|
}
|
|
noErr(t, state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID)))
|
|
noErr(t, state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID)))
|
|
noErr(t, state.UpsertAllocs(1000, []*structs.Allocation{alloc1, alloc2}))
|
|
|
|
// Add a planned eviction to alloc1
|
|
plan := ctx.Plan()
|
|
plan.NodeUpdate[nodes[0].Node.ID] = []*structs.Allocation{alloc1}
|
|
|
|
// Add a planned placement to node1
|
|
plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{
|
|
{
|
|
AllocatedResources: &structs.AllocatedResources{
|
|
Tasks: map[string]*structs.AllocatedTaskResources{
|
|
"web": {
|
|
Cpu: structs.AllocatedCpuResources{
|
|
CpuShares: 1024,
|
|
},
|
|
Memory: structs.AllocatedMemoryResources{
|
|
MemoryMB: 1024,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
proposed, err := ctx.ProposedAllocs(nodes[0].Node.ID)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(proposed) != 0 {
|
|
t.Fatalf("bad: %#v", proposed)
|
|
}
|
|
|
|
proposed, err = ctx.ProposedAllocs(nodes[1].Node.ID)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(proposed) != 2 {
|
|
t.Fatalf("bad: %#v", proposed)
|
|
}
|
|
}
|
|
|
|
// TestEvalContext_ProposedAlloc_EvictPreempt asserts both Evicted and
|
|
// Preempted allocs are removed from the allocs propsed for a node.
|
|
//
|
|
// See https://github.com/hashicorp/nomad/issues/6787
|
|
//
|
|
func TestEvalContext_ProposedAlloc_EvictPreempt(t *testing.T) {
|
|
t.Parallel()
|
|
state, ctx := testContext(t)
|
|
nodes := []*RankedNode{
|
|
{
|
|
Node: &structs.Node{
|
|
ID: uuid.Generate(),
|
|
NodeResources: &structs.NodeResources{
|
|
Cpu: structs.NodeCpuResources{
|
|
CpuShares: 1024 * 3,
|
|
},
|
|
Memory: structs.NodeMemoryResources{
|
|
MemoryMB: 1024 * 3,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Add existing allocations
|
|
j1, j2, j3 := mock.Job(), mock.Job(), mock.Job()
|
|
allocEvict := &structs.Allocation{
|
|
ID: uuid.Generate(),
|
|
Namespace: structs.DefaultNamespace,
|
|
EvalID: uuid.Generate(),
|
|
NodeID: nodes[0].Node.ID,
|
|
JobID: j1.ID,
|
|
Job: j1,
|
|
AllocatedResources: &structs.AllocatedResources{
|
|
Tasks: map[string]*structs.AllocatedTaskResources{
|
|
"web": {
|
|
Cpu: structs.AllocatedCpuResources{
|
|
CpuShares: 1024,
|
|
},
|
|
Memory: structs.AllocatedMemoryResources{
|
|
MemoryMB: 1024,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DesiredStatus: structs.AllocDesiredStatusRun,
|
|
ClientStatus: structs.AllocClientStatusPending,
|
|
TaskGroup: "web",
|
|
}
|
|
allocPreempt := &structs.Allocation{
|
|
ID: uuid.Generate(),
|
|
Namespace: structs.DefaultNamespace,
|
|
EvalID: uuid.Generate(),
|
|
NodeID: nodes[0].Node.ID,
|
|
JobID: j2.ID,
|
|
Job: j2,
|
|
AllocatedResources: &structs.AllocatedResources{
|
|
Tasks: map[string]*structs.AllocatedTaskResources{
|
|
"web": {
|
|
Cpu: structs.AllocatedCpuResources{
|
|
CpuShares: 1024,
|
|
},
|
|
Memory: structs.AllocatedMemoryResources{
|
|
MemoryMB: 1024,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DesiredStatus: structs.AllocDesiredStatusRun,
|
|
ClientStatus: structs.AllocClientStatusPending,
|
|
TaskGroup: "web",
|
|
}
|
|
allocPropose := &structs.Allocation{
|
|
ID: uuid.Generate(),
|
|
Namespace: structs.DefaultNamespace,
|
|
EvalID: uuid.Generate(),
|
|
NodeID: nodes[0].Node.ID,
|
|
JobID: j3.ID,
|
|
Job: j3,
|
|
AllocatedResources: &structs.AllocatedResources{
|
|
Tasks: map[string]*structs.AllocatedTaskResources{
|
|
"web": {
|
|
Cpu: structs.AllocatedCpuResources{
|
|
CpuShares: 1024,
|
|
},
|
|
Memory: structs.AllocatedMemoryResources{
|
|
MemoryMB: 1024,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DesiredStatus: structs.AllocDesiredStatusRun,
|
|
ClientStatus: structs.AllocClientStatusPending,
|
|
TaskGroup: "web",
|
|
}
|
|
require.NoError(t, state.UpsertJobSummary(998, mock.JobSummary(allocEvict.JobID)))
|
|
require.NoError(t, state.UpsertJobSummary(999, mock.JobSummary(allocPreempt.JobID)))
|
|
require.NoError(t, state.UpsertJobSummary(999, mock.JobSummary(allocPropose.JobID)))
|
|
require.NoError(t, state.UpsertAllocs(1000, []*structs.Allocation{allocEvict, allocPreempt, allocPropose}))
|
|
|
|
// Plan to evict one alloc and preempt another
|
|
plan := ctx.Plan()
|
|
plan.NodePreemptions[nodes[0].Node.ID] = []*structs.Allocation{allocEvict}
|
|
plan.NodeUpdate[nodes[0].Node.ID] = []*structs.Allocation{allocPreempt}
|
|
|
|
proposed, err := ctx.ProposedAllocs(nodes[0].Node.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, proposed, 1)
|
|
}
|
|
|
|
func TestEvalEligibility_JobStatus(t *testing.T) {
|
|
e := NewEvalEligibility()
|
|
cc := "v1:100"
|
|
|
|
// Get the job before its been set.
|
|
if status := e.JobStatus(cc); status != EvalComputedClassUnknown {
|
|
t.Fatalf("JobStatus() returned %v; want %v", status, EvalComputedClassUnknown)
|
|
}
|
|
|
|
// Set the job and get its status.
|
|
e.SetJobEligibility(false, cc)
|
|
if status := e.JobStatus(cc); status != EvalComputedClassIneligible {
|
|
t.Fatalf("JobStatus() returned %v; want %v", status, EvalComputedClassIneligible)
|
|
}
|
|
|
|
e.SetJobEligibility(true, cc)
|
|
if status := e.JobStatus(cc); status != EvalComputedClassEligible {
|
|
t.Fatalf("JobStatus() returned %v; want %v", status, EvalComputedClassEligible)
|
|
}
|
|
}
|
|
|
|
func TestEvalEligibility_TaskGroupStatus(t *testing.T) {
|
|
e := NewEvalEligibility()
|
|
cc := "v1:100"
|
|
tg := "foo"
|
|
|
|
// Get the tg before its been set.
|
|
if status := e.TaskGroupStatus(tg, cc); status != EvalComputedClassUnknown {
|
|
t.Fatalf("TaskGroupStatus() returned %v; want %v", status, EvalComputedClassUnknown)
|
|
}
|
|
|
|
// Set the tg and get its status.
|
|
e.SetTaskGroupEligibility(false, tg, cc)
|
|
if status := e.TaskGroupStatus(tg, cc); status != EvalComputedClassIneligible {
|
|
t.Fatalf("TaskGroupStatus() returned %v; want %v", status, EvalComputedClassIneligible)
|
|
}
|
|
|
|
e.SetTaskGroupEligibility(true, tg, cc)
|
|
if status := e.TaskGroupStatus(tg, cc); status != EvalComputedClassEligible {
|
|
t.Fatalf("TaskGroupStatus() returned %v; want %v", status, EvalComputedClassEligible)
|
|
}
|
|
}
|
|
|
|
func TestEvalEligibility_SetJob(t *testing.T) {
|
|
e := NewEvalEligibility()
|
|
ne1 := &structs.Constraint{
|
|
LTarget: "${attr.kernel.name}",
|
|
RTarget: "linux",
|
|
Operand: "=",
|
|
}
|
|
e1 := &structs.Constraint{
|
|
LTarget: "${attr.unique.kernel.name}",
|
|
RTarget: "linux",
|
|
Operand: "=",
|
|
}
|
|
e2 := &structs.Constraint{
|
|
LTarget: "${meta.unique.key_foo}",
|
|
RTarget: "linux",
|
|
Operand: "<",
|
|
}
|
|
e3 := &structs.Constraint{
|
|
LTarget: "${meta.unique.key_foo}",
|
|
RTarget: "Windows",
|
|
Operand: "<",
|
|
}
|
|
|
|
job := mock.Job()
|
|
jobCon := []*structs.Constraint{ne1, e1, e2}
|
|
job.Constraints = jobCon
|
|
|
|
// Set the task constraints
|
|
tg := job.TaskGroups[0]
|
|
tg.Constraints = []*structs.Constraint{e1}
|
|
tg.Tasks[0].Constraints = []*structs.Constraint{e3}
|
|
|
|
e.SetJob(job)
|
|
if !e.HasEscaped() {
|
|
t.Fatalf("HasEscaped() should be true")
|
|
}
|
|
|
|
if !e.jobEscaped {
|
|
t.Fatalf("SetJob() should mark job as escaped")
|
|
}
|
|
if escaped, ok := e.tgEscapedConstraints[tg.Name]; !ok || !escaped {
|
|
t.Fatalf("SetJob() should mark task group as escaped")
|
|
}
|
|
}
|
|
|
|
func TestEvalEligibility_GetClasses(t *testing.T) {
|
|
e := NewEvalEligibility()
|
|
e.SetJobEligibility(true, "v1:1")
|
|
e.SetJobEligibility(false, "v1:2")
|
|
e.SetTaskGroupEligibility(true, "foo", "v1:3")
|
|
e.SetTaskGroupEligibility(false, "bar", "v1:4")
|
|
e.SetTaskGroupEligibility(true, "bar", "v1:5")
|
|
|
|
// Mark an existing eligible class as ineligible in the TG.
|
|
e.SetTaskGroupEligibility(false, "fizz", "v1:1")
|
|
e.SetTaskGroupEligibility(false, "fizz", "v1:3")
|
|
|
|
expClasses := map[string]bool{
|
|
"v1:1": false,
|
|
"v1:2": false,
|
|
"v1:3": true,
|
|
"v1:4": false,
|
|
"v1:5": true,
|
|
}
|
|
|
|
actClasses := e.GetClasses()
|
|
require.Equal(t, expClasses, actClasses)
|
|
}
|
|
func TestEvalEligibility_GetClasses_JobEligible_TaskGroupIneligible(t *testing.T) {
|
|
e := NewEvalEligibility()
|
|
e.SetJobEligibility(true, "v1:1")
|
|
e.SetTaskGroupEligibility(false, "foo", "v1:1")
|
|
|
|
e.SetJobEligibility(true, "v1:2")
|
|
e.SetTaskGroupEligibility(false, "foo", "v1:2")
|
|
e.SetTaskGroupEligibility(true, "bar", "v1:2")
|
|
|
|
e.SetJobEligibility(true, "v1:3")
|
|
e.SetTaskGroupEligibility(false, "foo", "v1:3")
|
|
e.SetTaskGroupEligibility(false, "bar", "v1:3")
|
|
|
|
expClasses := map[string]bool{
|
|
"v1:1": false,
|
|
"v1:2": true,
|
|
"v1:3": false,
|
|
}
|
|
|
|
actClasses := e.GetClasses()
|
|
require.Equal(t, expClasses, actClasses)
|
|
}
|