scheduler: testing utility methods

This commit is contained in:
Armon Dadgar
2015-08-13 16:25:59 -07:00
parent 51edb53fb5
commit 057d226d3f
4 changed files with 255 additions and 105 deletions

View File

@@ -106,7 +106,9 @@ func Job() *structs.Job {
Meta: map[string]string{
"owner": "armon",
},
Status: structs.JobStatusPending,
Status: structs.JobStatusPending,
CreateIndex: 42,
ModifyIndex: 99,
}
return job
}

View File

@@ -42,7 +42,7 @@ func (s *ServiceScheduler) Process(eval *structs.Evaluation) error {
case structs.EvalTriggerJobRegister:
return s.handleJobRegister(eval)
case structs.EvalTriggerJobDeregister:
return s.handleJobDeregister(eval)
return s.evictJobAllocs(eval)
case structs.EvalTriggerNodeUpdate:
return s.handleNodeUpdate(eval)
default:
@@ -80,7 +80,7 @@ START:
// If there is nothing required for this job, treat like a deregister
if len(groups) == 0 {
return s.handleJobDeregister(eval)
return s.evictJobAllocs(eval)
}
// Lookup the allocations by JobID
@@ -292,8 +292,15 @@ func (s *ServiceScheduler) planAllocations(stack *IteratorStack, job *structs.Jo
return nil
}
// handleJobDeregister is used to handle a job being deregistered
func (s *ServiceScheduler) handleJobDeregister(eval *structs.Evaluation) error {
// handleNodeUpdate is used to handle an update to a node status where
// there is an existing allocation for this job
func (s *ServiceScheduler) handleNodeUpdate(eval *structs.Evaluation) error {
// TODO
return nil
}
// evictJobAllocs is used to evict all job allocations
func (s *ServiceScheduler) evictJobAllocs(eval *structs.Evaluation) error {
START:
// Lookup the allocations by JobID
allocs, err := s.state.AllocsByJob(eval.JobID)
@@ -335,103 +342,3 @@ START:
}
return nil
}
// handleNodeUpdate is used to handle an update to a node status where
// there is an existing allocation for this job
func (s *ServiceScheduler) handleNodeUpdate(eval *structs.Evaluation) error {
// TODO
return nil
}
// materializeTaskGroups is used to materialize all the task groups
// a job requires. This is used to do the count expansion.
func materializeTaskGroups(job *structs.Job) map[string]*structs.TaskGroup {
out := make(map[string]*structs.TaskGroup)
for _, tg := range job.TaskGroups {
for i := 0; i < tg.Count; i++ {
name := fmt.Sprintf("%s.%s[%d]", job.Name, tg.Name, i)
out[name] = tg
}
}
return out
}
// indexAllocs is used to index a list of allocations by name
func indexAllocs(allocs []*structs.Allocation) map[string][]*structs.Allocation {
out := make(map[string][]*structs.Allocation)
for _, alloc := range allocs {
name := alloc.Name
out[name] = append(out[name], alloc)
}
return out
}
// allocNameID is a tuple of the allocation name and ID
type allocNameID struct {
Name string
ID string
}
// diffAllocs is used to do a set difference between the target allocations
// and the existing allocations. This returns 4 sets of results, the list of
// named task groups that need to be placed (no existing allocation), the
// allocations that need to be updated (job definition is newer), the allocs
// that need to be evicted (no longer required), and those that should be
// ignored.
func diffAllocs(job *structs.Job,
required map[string]*structs.TaskGroup,
existing map[string][]*structs.Allocation) (place, update, evict, ignore []allocNameID) {
// Scan the existing updates
for name, existList := range existing {
for _, exist := range existList {
// Check for the definition in the required set
_, ok := required[name]
// If not required, we evict
if !ok {
evict = append(evict, allocNameID{name, exist.ID})
continue
}
// If the definition is updated we need to update
// XXX: This is an extremely conservative approach. We can check
// if the job definition has changed in a way that affects
// this allocation and potentially ignore it.
if job.ModifyIndex != exist.Job.ModifyIndex {
update = append(update, allocNameID{name, exist.ID})
continue
}
// Everything is up-to-date
ignore = append(ignore, allocNameID{name, exist.ID})
}
}
// Scan the required groups
for name := range required {
// Check for an existing allocation
_, ok := existing[name]
// Require a placement if no existing allocation. If there
// is an existing allocation, we would have checked for a potential
// update or ignore above.
if !ok {
place = append(place, allocNameID{name, ""})
}
}
return
}
// addEvictsToPlan is used to add all the evictions to the plan
func addEvictsToPlan(plan *structs.Plan,
evicts []allocNameID, indexed map[string][]*structs.Allocation) {
for _, evict := range evicts {
list := indexed[evict.Name]
for _, alloc := range list {
if alloc.ID != evict.ID {
continue
}
plan.AppendEvict(alloc)
}
}
}

99
scheduler/util.go Normal file
View File

@@ -0,0 +1,99 @@
package scheduler
import (
"fmt"
"github.com/hashicorp/nomad/nomad/structs"
)
// allocNameID is a tuple of the allocation name and potential alloc ID
type allocNameID struct {
Name string
ID string
}
// materializeTaskGroups is used to materialize all the task groups
// a job requires. This is used to do the count expansion.
func materializeTaskGroups(job *structs.Job) map[string]*structs.TaskGroup {
out := make(map[string]*structs.TaskGroup)
for _, tg := range job.TaskGroups {
for i := 0; i < tg.Count; i++ {
name := fmt.Sprintf("%s.%s[%d]", job.Name, tg.Name, i)
out[name] = tg
}
}
return out
}
// indexAllocs is used to index a list of allocations by name
func indexAllocs(allocs []*structs.Allocation) map[string][]*structs.Allocation {
out := make(map[string][]*structs.Allocation)
for _, alloc := range allocs {
name := alloc.Name
out[name] = append(out[name], alloc)
}
return out
}
// diffAllocs is used to do a set difference between the target allocations
// and the existing allocations. This returns 4 sets of results, the list of
// named task groups that need to be placed (no existing allocation), the
// allocations that need to be updated (job definition is newer), the allocs
// that need to be evicted (no longer required), and those that should be
// ignored.
func diffAllocs(job *structs.Job,
required map[string]*structs.TaskGroup,
existing map[string][]*structs.Allocation) (place, update, evict, ignore []allocNameID) {
// Scan the existing updates
for name, existList := range existing {
for _, exist := range existList {
// Check for the definition in the required set
_, ok := required[name]
// If not required, we evict
if !ok {
evict = append(evict, allocNameID{name, exist.ID})
continue
}
// If the definition is updated we need to update
// XXX: This is an extremely conservative approach. We can check
// if the job definition has changed in a way that affects
// this allocation and potentially ignore it.
if job.ModifyIndex != exist.Job.ModifyIndex {
update = append(update, allocNameID{name, exist.ID})
continue
}
// Everything is up-to-date
ignore = append(ignore, allocNameID{name, exist.ID})
}
}
// Scan the required groups
for name := range required {
// Check for an existing allocation
_, ok := existing[name]
// Require a placement if no existing allocation. If there
// is an existing allocation, we would have checked for a potential
// update or ignore above.
if !ok {
place = append(place, allocNameID{name, ""})
}
}
return
}
// addEvictsToPlan is used to add all the evictions to the plan
func addEvictsToPlan(plan *structs.Plan,
evicts []allocNameID, indexed map[string][]*structs.Allocation) {
for _, evict := range evicts {
list := indexed[evict.Name]
for _, alloc := range list {
if alloc.ID == evict.ID {
plan.AppendEvict(alloc)
}
}
}
}

142
scheduler/util_test.go Normal file
View File

@@ -0,0 +1,142 @@
package scheduler
import (
"fmt"
"testing"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
)
func TestMaterializeTaskGroups(t *testing.T) {
job := mock.Job()
index := materializeTaskGroups(job)
if len(index) != 10 {
t.Fatalf("Bad: %#v", index)
}
for i := 0; i < 10; i++ {
name := fmt.Sprintf("my-job.web[%d]", i)
tg, ok := index[name]
if !ok {
t.Fatalf("bad")
}
if tg != job.TaskGroups[0] {
t.Fatalf("bad")
}
}
}
func TestIndexAllocs(t *testing.T) {
allocs := []*structs.Allocation{
&structs.Allocation{Name: "foo"},
&structs.Allocation{Name: "foo"},
&structs.Allocation{Name: "bar"},
}
index := indexAllocs(allocs)
if len(index) != 2 {
t.Fatalf("bad: %#v", index)
}
if len(index["foo"]) != 2 {
t.Fatalf("bad: %#v", index)
}
if len(index["bar"]) != 1 {
t.Fatalf("bad: %#v", index)
}
}
func TestDiffAllocs(t *testing.T) {
job := mock.Job()
required := materializeTaskGroups(job)
// The "old" job has a previous modify index
oldJob := new(structs.Job)
*oldJob = *job
oldJob.ModifyIndex -= 1
allocs := []*structs.Allocation{
// Update the 1st
&structs.Allocation{
ID: mock.GenerateUUID(),
NodeID: "zip",
Name: "my-job.web[0]",
Job: oldJob,
},
// Ignore the 2rd
&structs.Allocation{
ID: mock.GenerateUUID(),
NodeID: "zip",
Name: "my-job.web[1]",
Job: job,
},
// Evict 11th
&structs.Allocation{
ID: mock.GenerateUUID(),
NodeID: "zip",
Name: "my-job.web[10]",
},
}
existing := indexAllocs(allocs)
place, update, evict, ignore := diffAllocs(job, required, existing)
// We should update the first alloc
if len(update) != 1 || update[0].ID != allocs[0].ID {
t.Fatalf("bad: %#v", update)
}
// We should ignore the second alloc
if len(ignore) != 1 || ignore[0].ID != allocs[1].ID {
t.Fatalf("bad: %#v", ignore)
}
// We should evict the 3rd alloc
if len(evict) != 1 || evict[0].ID != allocs[2].ID {
t.Fatalf("bad: %#v", evict)
}
// We should place 8
if len(place) != 8 {
t.Fatalf("bad: %#v", place)
}
}
func TestAddEvictsToPlan(t *testing.T) {
allocs := []*structs.Allocation{
&structs.Allocation{
ID: mock.GenerateUUID(),
NodeID: "zip",
Name: "foo",
},
&structs.Allocation{
ID: mock.GenerateUUID(),
NodeID: "zip",
Name: "foo",
},
&structs.Allocation{
ID: mock.GenerateUUID(),
NodeID: "zip",
Name: "bar",
},
}
plan := &structs.Plan{
NodeEvict: make(map[string][]string),
}
index := indexAllocs(allocs)
evict := []allocNameID{
allocNameID{Name: "foo", ID: allocs[0].ID},
allocNameID{Name: "bar", ID: allocs[2].ID},
}
addEvictsToPlan(plan, evict, index)
nodeEvict := plan.NodeEvict["zip"]
if len(nodeEvict) != 2 {
t.Fatalf("bad: %#v %v", plan, nodeEvict)
}
if nodeEvict[0] != allocs[0].ID || nodeEvict[1] != allocs[2].ID {
t.Fatalf("bad: %v", nodeEvict)
}
}