2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2022-09-15 15:12:20 +00:00
|
|
|
package spread
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/nomad/api"
|
|
|
|
"github.com/hashicorp/nomad/e2e/e2eutil"
|
|
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
|
|
"github.com/shoenig/test/must"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
evenJobFilePath = "./input/even_spread.nomad"
|
|
|
|
multipleJobFilePath = "./input/multiple_spread.nomad"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestSpread(t *testing.T) {
|
|
|
|
|
|
|
|
nomadClient := e2eutil.NomadClient(t)
|
|
|
|
e2eutil.WaitForLeader(t, nomadClient)
|
|
|
|
e2eutil.WaitForNodesReady(t, nomadClient, 4)
|
|
|
|
|
|
|
|
// Run our test cases.
|
|
|
|
t.Run("TestSpread_Even", testSpreadEven)
|
|
|
|
t.Run("TestSpread_Multiple", testSpreadMultiple)
|
|
|
|
}
|
|
|
|
|
|
|
|
func testSpreadEven(t *testing.T) {
|
|
|
|
|
|
|
|
nomadClient := e2eutil.NomadClient(t)
|
|
|
|
|
|
|
|
// Generate a job ID and register the test job.
|
|
|
|
jobID := "spread-" + uuid.Short()
|
|
|
|
allocs := e2eutil.RegisterAndWaitForAllocs(t, nomadClient, evenJobFilePath, jobID, "")
|
|
|
|
|
|
|
|
// Ensure the test cleans its own job and allocations fully, so it does not
|
|
|
|
// impact other spread tests.
|
|
|
|
t.Cleanup(func() { cleanupJob(t, nomadClient, jobID, allocs) })
|
|
|
|
|
|
|
|
dcToAllocs := make(map[string]int)
|
|
|
|
|
|
|
|
for _, allocStub := range allocs {
|
|
|
|
alloc, _, err := nomadClient.Allocations().Info(allocStub.ID, nil)
|
|
|
|
must.NoError(t, err)
|
2022-10-06 13:27:36 +00:00
|
|
|
must.Greater(t, 0, len(alloc.Metrics.ScoreMetaData))
|
2022-09-15 15:12:20 +00:00
|
|
|
node, _, err := nomadClient.Nodes().Info(alloc.NodeID, nil)
|
|
|
|
must.NoError(t, err)
|
|
|
|
dcToAllocs[node.Datacenter]++
|
|
|
|
}
|
|
|
|
|
|
|
|
must.Eq(t, map[string]int{"dc1": 3, "dc2": 3}, dcToAllocs)
|
|
|
|
}
|
|
|
|
|
|
|
|
func testSpreadMultiple(t *testing.T) {
|
|
|
|
|
|
|
|
nomadClient := e2eutil.NomadClient(t)
|
|
|
|
|
|
|
|
// Generate a job ID and register the test job.
|
|
|
|
jobID := "spread-" + uuid.Short()
|
|
|
|
allocs := e2eutil.RegisterAndWaitForAllocs(t, nomadClient, multipleJobFilePath, jobID, "")
|
|
|
|
|
|
|
|
// Ensure the test cleans its own job and allocations fully, so it does not
|
|
|
|
// impact other spread tests.
|
|
|
|
t.Cleanup(func() { cleanupJob(t, nomadClient, jobID, allocs) })
|
|
|
|
|
|
|
|
// Verify spread score and alloc distribution
|
|
|
|
dcToAllocs := make(map[string]int)
|
|
|
|
rackToAllocs := make(map[string]int)
|
|
|
|
allocMetrics := make(map[string]*api.AllocationMetric)
|
|
|
|
|
|
|
|
for _, allocStub := range allocs {
|
|
|
|
alloc, _, err := nomadClient.Allocations().Info(allocStub.ID, nil)
|
|
|
|
must.NoError(t, err)
|
2022-10-06 13:27:36 +00:00
|
|
|
must.Greater(t, 0, len(alloc.Metrics.ScoreMetaData))
|
2022-09-15 15:12:20 +00:00
|
|
|
allocMetrics[allocStub.ID] = alloc.Metrics
|
|
|
|
|
|
|
|
node, _, err := nomadClient.Nodes().Info(alloc.NodeID, nil)
|
|
|
|
must.NoError(t, err)
|
|
|
|
dcToAllocs[node.Datacenter]++
|
|
|
|
|
|
|
|
if rack := node.Meta["rack"]; rack != "" {
|
|
|
|
rackToAllocs[rack]++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
failureReport := report(allocMetrics)
|
|
|
|
|
|
|
|
must.Eq(t, map[string]int{"dc1": 5, "dc2": 5}, dcToAllocs, failureReport)
|
|
|
|
must.Eq(t, map[string]int{"r1": 7, "r2": 3}, rackToAllocs, failureReport)
|
|
|
|
}
|
|
|
|
|
|
|
|
func cleanupJob(t *testing.T, nomadClient *api.Client, jobID string, allocs []*api.AllocationListStub) {
|
|
|
|
|
|
|
|
_, _, err := nomadClient.Jobs().Deregister(jobID, true, nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Ensure that all allocations have been removed from state. This is an
|
|
|
|
// important aspect of the cleaning required which allows the spread
|
|
|
|
// test to run successfully.
|
|
|
|
assert.Eventually(t, func() bool {
|
|
|
|
|
|
|
|
// Run the garbage collector to remove all terminal allocations.
|
|
|
|
must.NoError(t, nomadClient.System().GarbageCollect())
|
|
|
|
|
|
|
|
for _, allocStub := range allocs {
|
|
|
|
_, _, err := nomadClient.Allocations().Info(allocStub.ID, nil)
|
|
|
|
if err == nil {
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
if !strings.Contains(err.Error(), "alloc not found") {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}, 10*time.Second, 200*time.Millisecond)
|
|
|
|
}
|
|
|
|
|
2023-01-17 18:13:57 +00:00
|
|
|
func report(metrics map[string]*api.AllocationMetric) must.Setting {
|
2022-09-15 15:12:20 +00:00
|
|
|
var s strings.Builder
|
|
|
|
for allocID, m := range metrics {
|
|
|
|
s.WriteString("Alloc ID: " + allocID + "\n")
|
|
|
|
s.WriteString(fmt.Sprintf(" NodesEvaluated: %d\n", m.NodesEvaluated))
|
|
|
|
s.WriteString(fmt.Sprintf(" NodesAvailable: %#v\n", m.NodesAvailable))
|
|
|
|
s.WriteString(fmt.Sprintf(" ClassFiltered: %#v\n", m.ClassFiltered))
|
|
|
|
s.WriteString(fmt.Sprintf(" ConstraintFiltered: %#v\n", m.ConstraintFiltered))
|
|
|
|
s.WriteString(fmt.Sprintf(" NodesExhausted: %d\n", m.NodesExhausted))
|
|
|
|
s.WriteString(fmt.Sprintf(" ClassExhausted: %#v\n", m.ClassExhausted))
|
|
|
|
s.WriteString(fmt.Sprintf(" DimensionExhausted: %#v\n", m.DimensionExhausted))
|
|
|
|
s.WriteString(fmt.Sprintf(" QuotaExhausted: %#v\n", m.QuotaExhausted))
|
|
|
|
for _, nodeMeta := range m.ScoreMetaData {
|
|
|
|
s.WriteString(fmt.Sprintf(" NodeID: %s, NormScore: %f, Scores: %#v\n",
|
|
|
|
nodeMeta.NodeID, nodeMeta.NormScore, nodeMeta.Scores))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return must.Sprint(s.String())
|
|
|
|
}
|