diff --git a/nomad/mock/mock.go b/nomad/mock/mock.go index 123e86b27..16c61a495 100644 --- a/nomad/mock/mock.go +++ b/nomad/mock/mock.go @@ -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 } diff --git a/scheduler/service_sched.go b/scheduler/service_sched.go index 795221dd5..88eb76cd5 100644 --- a/scheduler/service_sched.go +++ b/scheduler/service_sched.go @@ -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) - } - } -} diff --git a/scheduler/util.go b/scheduler/util.go new file mode 100644 index 000000000..81ccfbd73 --- /dev/null +++ b/scheduler/util.go @@ -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) + } + } + } +} diff --git a/scheduler/util_test.go b/scheduler/util_test.go new file mode 100644 index 000000000..851fcf096 --- /dev/null +++ b/scheduler/util_test.go @@ -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) + } +}