Skip to content

Commit

Permalink
Merge pull request #2418 from hashicorp/f-distinct-property
Browse files Browse the repository at this point in the history
`distinct_property` constraint
  • Loading branch information
dadgar authored Mar 10, 2017
2 parents be942df + 336a976 commit d0d3663
Show file tree
Hide file tree
Showing 10 changed files with 1,282 additions and 105 deletions.
12 changes: 9 additions & 3 deletions jobspec/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,13 @@ func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error {
// Check for invalid keys
valid := []string{
"attribute",
"distinct_hosts",
"distinct_property",
"operator",
"value",
"version",
"regexp",
"distinct_hosts",
"set_contains",
"value",
"version",
}
if err := checkHCLKeys(o.Val, valid); err != nil {
return err
Expand Down Expand Up @@ -467,6 +468,11 @@ func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error {
m["Operand"] = structs.ConstraintDistinctHosts
}

if property, ok := m[structs.ConstraintDistinctProperty]; ok {
m["Operand"] = structs.ConstraintDistinctProperty
m["LTarget"] = property
}

// Build the constraint
var c api.Constraint
if err := mapstructure.WeakDecode(m, &c); err != nil {
Expand Down
15 changes: 15 additions & 0 deletions jobspec/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,21 @@ func TestParse(t *testing.T) {
false,
},

{
"distinctProperty-constraint.hcl",
&api.Job{
ID: helper.StringToPtr("foo"),
Name: helper.StringToPtr("foo"),
Constraints: []*api.Constraint{
&api.Constraint{
Operand: structs.ConstraintDistinctProperty,
LTarget: "${meta.rack}",
},
},
},
false,
},

{
"periodic-cron.hcl",
&api.Job{
Expand Down
5 changes: 5 additions & 0 deletions jobspec/test-fixtures/distinctProperty-constraint.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
job "foo" {
constraint {
distinct_property = "${meta.rack}"
}
}
9 changes: 5 additions & 4 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3189,10 +3189,11 @@ func (ta *TaskArtifact) Validate() error {
}

const (
ConstraintDistinctHosts = "distinct_hosts"
ConstraintRegex = "regexp"
ConstraintVersion = "version"
ConstraintSetContains = "set_contains"
ConstraintDistinctProperty = "distinct_property"
ConstraintDistinctHosts = "distinct_hosts"
ConstraintRegex = "regexp"
ConstraintVersion = "version"
ConstraintSetContains = "set_contains"
)

// Constraints are used to restrict placement options.
Expand Down
144 changes: 125 additions & 19 deletions scheduler/feasible.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,10 @@ func (c *DriverChecker) hasDrivers(option *structs.Node) bool {
return true
}

// ProposedAllocConstraintIterator is a FeasibleIterator which returns nodes that
// match constraints that are not static such as Node attributes but are
// effected by proposed alloc placements. Examples are distinct_hosts and
// tenancy constraints. This is used to filter on job and task group
// constraints.
type ProposedAllocConstraintIterator struct {
// DistinctHostsIterator is a FeasibleIterator which returns nodes that pass the
// distinct_hosts constraint. The constraint ensures that multiple allocations
// do not exist on the same node.
type DistinctHostsIterator struct {
ctx Context
source FeasibleIterator
tg *structs.TaskGroup
Expand All @@ -159,44 +157,47 @@ type ProposedAllocConstraintIterator struct {
jobDistinctHosts bool
}

// NewProposedAllocConstraintIterator creates a ProposedAllocConstraintIterator
// from a source.
func NewProposedAllocConstraintIterator(ctx Context, source FeasibleIterator) *ProposedAllocConstraintIterator {
return &ProposedAllocConstraintIterator{
// NewDistinctHostsIterator creates a DistinctHostsIterator from a source.
func NewDistinctHostsIterator(ctx Context, source FeasibleIterator) *DistinctHostsIterator {
return &DistinctHostsIterator{
ctx: ctx,
source: source,
}
}

func (iter *ProposedAllocConstraintIterator) SetTaskGroup(tg *structs.TaskGroup) {
func (iter *DistinctHostsIterator) SetTaskGroup(tg *structs.TaskGroup) {
iter.tg = tg
iter.tgDistinctHosts = iter.hasDistinctHostsConstraint(tg.Constraints)
}

func (iter *ProposedAllocConstraintIterator) SetJob(job *structs.Job) {
func (iter *DistinctHostsIterator) SetJob(job *structs.Job) {
iter.job = job
iter.jobDistinctHosts = iter.hasDistinctHostsConstraint(job.Constraints)
}

func (iter *ProposedAllocConstraintIterator) hasDistinctHostsConstraint(constraints []*structs.Constraint) bool {
func (iter *DistinctHostsIterator) hasDistinctHostsConstraint(constraints []*structs.Constraint) bool {
for _, con := range constraints {
if con.Operand == structs.ConstraintDistinctHosts {
return true
}
}

return false
}

func (iter *ProposedAllocConstraintIterator) Next() *structs.Node {
func (iter *DistinctHostsIterator) Next() *structs.Node {
for {
// Get the next option from the source
option := iter.source.Next()

// Hot-path if the option is nil or there are no distinct_hosts constraints.
if option == nil || !(iter.jobDistinctHosts || iter.tgDistinctHosts) {
// Hot-path if the option is nil or there are no distinct_hosts or
// distinct_property constraints.
hosts := iter.jobDistinctHosts || iter.tgDistinctHosts
if option == nil || !hosts {
return option
}

// Check if the host constraints are satisfied
if !iter.satisfiesDistinctHosts(option) {
iter.ctx.Metrics().FilterNode(option, structs.ConstraintDistinctHosts)
continue
Expand All @@ -208,7 +209,7 @@ func (iter *ProposedAllocConstraintIterator) Next() *structs.Node {

// satisfiesDistinctHosts checks if the node satisfies a distinct_hosts
// constraint either specified at the job level or the TaskGroup level.
func (iter *ProposedAllocConstraintIterator) satisfiesDistinctHosts(option *structs.Node) bool {
func (iter *DistinctHostsIterator) satisfiesDistinctHosts(option *structs.Node) bool {
// Check if there is no constraint set.
if !(iter.jobDistinctHosts || iter.tgDistinctHosts) {
return true
Expand Down Expand Up @@ -237,8 +238,113 @@ func (iter *ProposedAllocConstraintIterator) satisfiesDistinctHosts(option *stru
return true
}

func (iter *ProposedAllocConstraintIterator) Reset() {
func (iter *DistinctHostsIterator) Reset() {
iter.source.Reset()
}

// DistinctPropertyIterator is a FeasibleIterator which returns nodes that pass the
// distinct_property constraint. The constraint ensures that multiple allocations
// do not use the same value of the given property.
type DistinctPropertyIterator struct {
ctx Context
source FeasibleIterator
tg *structs.TaskGroup
job *structs.Job

hasDistinctPropertyConstraints bool
jobPropertySets []*propertySet
groupPropertySets map[string][]*propertySet
}

// NewDistinctPropertyIterator creates a DistinctPropertyIterator from a source.
func NewDistinctPropertyIterator(ctx Context, source FeasibleIterator) *DistinctPropertyIterator {
return &DistinctPropertyIterator{
ctx: ctx,
source: source,
groupPropertySets: make(map[string][]*propertySet),
}
}

func (iter *DistinctPropertyIterator) SetTaskGroup(tg *structs.TaskGroup) {
iter.tg = tg

// Build the property set at the taskgroup level
if _, ok := iter.groupPropertySets[tg.Name]; !ok {
for _, c := range tg.Constraints {
if c.Operand != structs.ConstraintDistinctProperty {
continue
}

pset := NewPropertySet(iter.ctx, iter.job)
pset.SetTGConstraint(c, tg.Name)
iter.groupPropertySets[tg.Name] = append(iter.groupPropertySets[tg.Name], pset)
}
}

// Check if there is a distinct property
iter.hasDistinctPropertyConstraints = len(iter.jobPropertySets) != 0 || len(iter.groupPropertySets[tg.Name]) != 0
}

func (iter *DistinctPropertyIterator) SetJob(job *structs.Job) {
iter.job = job

// Build the property set at the job level
for _, c := range job.Constraints {
if c.Operand != structs.ConstraintDistinctProperty {
continue
}

pset := NewPropertySet(iter.ctx, job)
pset.SetJobConstraint(c)
iter.jobPropertySets = append(iter.jobPropertySets, pset)
}
}

func (iter *DistinctPropertyIterator) Next() *structs.Node {
for {
// Get the next option from the source
option := iter.source.Next()

// Hot path if there is nothing to check
if option == nil || !iter.hasDistinctPropertyConstraints {
return option
}

// Check if the constraints are met
if !iter.satisfiesProperties(option, iter.jobPropertySets) ||
!iter.satisfiesProperties(option, iter.groupPropertySets[iter.tg.Name]) {
continue
}

return option
}
}

// satisfiesProperties returns whether the option satisfies the set of
// properties. If not it will be filtered.
func (iter *DistinctPropertyIterator) satisfiesProperties(option *structs.Node, set []*propertySet) bool {
for _, ps := range set {
if satisfies, reason := ps.SatisfiesDistinctProperties(option, iter.tg.Name); !satisfies {
iter.ctx.Metrics().FilterNode(option, reason)
return false
}
}

return true
}

func (iter *DistinctPropertyIterator) Reset() {
iter.source.Reset()

for _, ps := range iter.jobPropertySets {
ps.PopulateProposed()
}

for _, sets := range iter.groupPropertySets {
for _, ps := range sets {
ps.PopulateProposed()
}
}
}

// ConstraintChecker is a FeasibilityChecker which returns nodes that match a
Expand Down Expand Up @@ -327,7 +433,7 @@ func resolveConstraintTarget(target string, node *structs.Node) (interface{}, bo
func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool {
// Check for constraints not handled by this checker.
switch operand {
case structs.ConstraintDistinctHosts:
case structs.ConstraintDistinctHosts, structs.ConstraintDistinctProperty:
return true
default:
break
Expand Down
Loading

0 comments on commit d0d3663

Please sign in to comment.