Skip to content

Commit

Permalink
system: re-evaluate node on feasibility changes (#11007)
Browse files Browse the repository at this point in the history
Fix a bug where system jobs may fail to be placed on a node that
initially was not eligible for system job placement.

This changes causes the reschedule to re-evaluate the node if any
attribute used in feasibility checks changes.

Fixes #8448
  • Loading branch information
Mahmood Ali authored Aug 10, 2021
1 parent dfb313a commit aad6d40
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changelog/11007.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
scheduler: Re-evaluate nodes for system jobs after attributes changes
```
58 changes: 52 additions & 6 deletions nomad/node_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package nomad
import (
"context"
"fmt"
"reflect"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -169,12 +170,7 @@ func (n *Node) Register(args *structs.NodeRegisterRequest, reply *structs.NodeUp
reply.NodeModifyIndex = index

// Check if we should trigger evaluations
originalStatus := structs.NodeStatusInit
if originalNode != nil {
originalStatus = originalNode.Status
}
transitionToReady := transitionedToReady(args.Node.Status, originalStatus)
if structs.ShouldDrainNode(args.Node.Status) || transitionToReady {
if shouldCreateNodeEval(originalNode, args.Node) {
evalIDs, evalIndex, err := n.createNodeEvals(args.Node.ID, index)
if err != nil {
n.logger.Error("eval creation failed", "error", err)
Expand Down Expand Up @@ -211,6 +207,56 @@ func (n *Node) Register(args *structs.NodeRegisterRequest, reply *structs.NodeUp
return nil
}

// shouldCreateNodeEval returns true if the node update may result into
// allocation updates, so the node should be re-evaluating.
//
// Such cases might be:
// * node health/drain status changes that may result into alloc rescheduling
// * node drivers or attributes changing that may cause system job placement changes
func shouldCreateNodeEval(original, updated *structs.Node) bool {
if structs.ShouldDrainNode(updated.Status) {
return true
}

if original == nil {
return transitionedToReady(updated.Status, structs.NodeStatusInit)
}

if transitionedToReady(updated.Status, original.Status) {
return true
}

// check fields used by the feasibility checks in ../scheduler/feasible.go,
// whether through a Constraint explicitly added by user or an implicit constraint
// added through a driver/volume check.
//
// Node Resources (e.g. CPU/Memory) are handled differently, using blocked evals,
// and not relevant in this check.
return !(original.ID == updated.ID &&
original.Datacenter == updated.Datacenter &&
original.Name == updated.Name &&
original.NodeClass == updated.NodeClass &&
reflect.DeepEqual(original.Attributes, updated.Attributes) &&
reflect.DeepEqual(original.Meta, updated.Meta) &&
reflect.DeepEqual(original.Drivers, updated.Drivers) &&
reflect.DeepEqual(original.HostVolumes, updated.HostVolumes) &&
equalDevices(original, updated))
}

func equalDevices(n1, n2 *structs.Node) bool {
// ignore super old nodes, mostly to avoid nil dereferencing
if n1.NodeResources == nil || n2.NodeResources == nil {
return n1.NodeResources == n2.NodeResources
}

// treat nil and empty value as equal
if len(n1.NodeResources.Devices) == 0 {
return len(n1.NodeResources.Devices) == len(n2.NodeResources.Devices)
}

return reflect.DeepEqual(n1.NodeResources.Devices, n2.NodeResources.Devices)
}

// updateNodeUpdateResponse assumes the n.srv.peerLock is held for reading.
func (n *Node) constructNodeServerInfoResponse(snap *state.StateSnapshot, reply *structs.NodeUpdateResponse) error {
reply.LeaderRPCAddr = string(n.srv.raft.Leader())
Expand Down
53 changes: 53 additions & 0 deletions nomad/node_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3602,3 +3602,56 @@ func TestClientEndpoint_EmitEvents(t *testing.T) {
require.Nil(err)
require.False(len(out.Events) < 2)
}

func TestClientEndpoint_ShouldCreateNodeEval(t *testing.T) {
t.Run("spurious changes don't require eval", func(t *testing.T) {
n1 := mock.Node()
n2 := n1.Copy()
n2.SecretID = uuid.Generate()
n2.Links["vault"] = "links don't get interpolated"
n2.ModifyIndex++

require.False(t, shouldCreateNodeEval(n1, n2))
})

positiveCases := []struct {
name string
updateFn func(n *structs.Node)
}{
{
"data center changes",
func(n *structs.Node) { n.Datacenter += "u" },
},
{
"attribute change",
func(n *structs.Node) { n.Attributes["test.attribute"] = "something" },
},
{
"meta change",
func(n *structs.Node) { n.Meta["test.meta"] = "something" },
},
{
"drivers health changed",
func(n *structs.Node) { n.Drivers["exec"].Detected = false },
},
{
"new drivers",
func(n *structs.Node) {
n.Drivers["newdriver"] = &structs.DriverInfo{
Detected: true,
Healthy: true,
}
},
},
}

for _, c := range positiveCases {
t.Run(c.name, func(t *testing.T) {
n1 := mock.Node()
n2 := n1.Copy()
c.updateFn(n2)

require.Truef(t, shouldCreateNodeEval(n1, n2), "node changed but without node eval: %v", pretty.Diff(n1, n2))
})
}
}

0 comments on commit aad6d40

Please sign in to comment.