// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 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) must.Greater(t, 0, len(alloc.Metrics.ScoreMetaData)) 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) must.Greater(t, 0, len(alloc.Metrics.ScoreMetaData)) 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) } func report(metrics map[string]*api.AllocationMetric) must.Setting { 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()) }