// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package scheduler import ( "fmt" "testing" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/iterator" "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/shoenig/test/must" ) func TestScheduler_JobRegister_MemoryMaxHonored(t *testing.T) { ci.Parallel(t) // Test node pools. poolNoSchedConfig := mock.NodePool() poolNoSchedConfig.SchedulerConfiguration = nil poolWithMemOversub := mock.NodePool() poolWithMemOversub.SchedulerConfiguration = &structs.NodePoolSchedulerConfiguration{ MemoryOversubscriptionEnabled: pointer.Of(true), } poolNoMemOversub := mock.NodePool() poolNoMemOversub.SchedulerConfiguration = &structs.NodePoolSchedulerConfiguration{ MemoryOversubscriptionEnabled: pointer.Of(false), } cases := []struct { name string nodePool string cpu int memory int memoryMax int memoryOversubscriptionEnabled bool expectedTaskMemoryMax int // expectedTotalMemoryMax should be SUM(MAX(memory, memoryMax)) for all // tasks if memory oversubscription is enabled and SUM(memory) if it's // disabled. expectedTotalMemoryMax int }{ { name: "plain no max", nodePool: poolNoSchedConfig.Name, cpu: 100, memory: 200, memoryMax: 0, memoryOversubscriptionEnabled: true, expectedTaskMemoryMax: 0, expectedTotalMemoryMax: 200, }, { name: "with max", nodePool: poolNoSchedConfig.Name, cpu: 100, memory: 200, memoryMax: 300, memoryOversubscriptionEnabled: true, expectedTaskMemoryMax: 300, expectedTotalMemoryMax: 300, }, { name: "with max but disabled", nodePool: poolNoSchedConfig.Name, cpu: 100, memory: 200, memoryMax: 300, memoryOversubscriptionEnabled: false, expectedTaskMemoryMax: 0, expectedTotalMemoryMax: 200, // same as no max }, { name: "with max and enabled by node pool", nodePool: poolWithMemOversub.Name, cpu: 100, memory: 200, memoryMax: 300, memoryOversubscriptionEnabled: false, expectedTaskMemoryMax: 300, expectedTotalMemoryMax: 300, }, { name: "with max but disabled by node pool", nodePool: poolNoMemOversub.Name, cpu: 100, memory: 200, memoryMax: 300, memoryOversubscriptionEnabled: true, expectedTaskMemoryMax: 0, expectedTotalMemoryMax: 200, // same as no max }, } jobTypes := []string{ "batch", "service", "sysbatch", "system", } for _, jobType := range jobTypes { for _, c := range cases { t.Run(fmt.Sprintf("%s/%s", jobType, c.name), func(t *testing.T) { h := NewHarness(t) // Create node pools. nodePools := []*structs.NodePool{ poolNoSchedConfig, poolWithMemOversub, poolNoMemOversub, } h.State.UpsertNodePools(structs.MsgTypeTestSetup, h.NextIndex(), nodePools) // Create some nodes. for i := 0; i < 3; i++ { node := mock.Node() node.NodePool = c.nodePool must.NoError(t, h.State.UpsertNode(structs.MsgTypeTestSetup, h.NextIndex(), node)) } // Set global scheduler configuration. h.State.SchedulerSetConfig(h.NextIndex(), &structs.SchedulerConfiguration{ MemoryOversubscriptionEnabled: c.memoryOversubscriptionEnabled, }) // Create test job. var job *structs.Job switch jobType { case "batch": job = mock.BatchJob() case "service": job = mock.Job() case "sysbatch": job = mock.SystemBatchJob() case "system": job = mock.SystemJob() } job.TaskGroups[0].Count = 1 job.NodePool = c.nodePool task := job.TaskGroups[0].Tasks[0].Name res := job.TaskGroups[0].Tasks[0].Resources res.CPU = c.cpu res.MemoryMB = c.memory res.MemoryMaxMB = c.memoryMax must.NoError(t, h.State.UpsertJob(structs.MsgTypeTestSetup, h.NextIndex(), nil, job)) // Create a mock evaluation to register the job eval := &structs.Evaluation{ Namespace: structs.DefaultNamespace, ID: uuid.Generate(), Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, Status: structs.EvalStatusPending, } must.NoError(t, h.State.UpsertEvals(structs.MsgTypeTestSetup, h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation var scheduler Factory switch jobType { case "batch": scheduler = NewBatchScheduler case "service": scheduler = NewServiceScheduler case "sysbatch": scheduler = NewSysBatchScheduler case "system": scheduler = NewSystemScheduler } err := h.Process(scheduler, eval) must.NoError(t, err) must.Len(t, 1, h.Plans) allocs, err := h.State.AllocsByJob(nil, job.Namespace, job.ID, false) must.NoError(t, err) // Ensure all allocations placed var expectedAllocCount int switch jobType { case "batch", "service": expectedAllocCount = 1 case "system", "sysbatch": nodes, err := h.State.NodesByNodePool(nil, job.NodePool) must.NoError(t, err) expectedAllocCount = iterator.Len(nodes) } must.Len(t, expectedAllocCount, allocs) alloc := allocs[0] // checking new resources field deprecated Resources fields must.Eq(t, int64(c.cpu), alloc.AllocatedResources.Tasks[task].Cpu.CpuShares) must.Eq(t, int64(c.memory), alloc.AllocatedResources.Tasks[task].Memory.MemoryMB) must.Eq(t, int64(c.expectedTaskMemoryMax), alloc.AllocatedResources.Tasks[task].Memory.MemoryMaxMB) // checking old deprecated Resources fields must.Eq(t, c.cpu, alloc.TaskResources[task].CPU) must.Eq(t, c.memory, alloc.TaskResources[task].MemoryMB) must.Eq(t, c.expectedTaskMemoryMax, alloc.TaskResources[task].MemoryMaxMB) // check total resource fields - alloc.Resources deprecated field, no modern equivalent must.Eq(t, c.cpu, alloc.Resources.CPU) must.Eq(t, c.memory, alloc.Resources.MemoryMB) must.Eq(t, c.expectedTotalMemoryMax, alloc.Resources.MemoryMaxMB) }) } } }