github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/scheduler/rank_test.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/hashicorp/nomad/helper/uuid"
     7  	"github.com/hashicorp/nomad/nomad/mock"
     8  	"github.com/hashicorp/nomad/nomad/structs"
     9  	require "github.com/stretchr/testify/require"
    10  )
    11  
    12  func TestFeasibleRankIterator(t *testing.T) {
    13  	_, ctx := testContext(t)
    14  	var nodes []*structs.Node
    15  	for i := 0; i < 10; i++ {
    16  		nodes = append(nodes, mock.Node())
    17  	}
    18  	static := NewStaticIterator(ctx, nodes)
    19  
    20  	feasible := NewFeasibleRankIterator(ctx, static)
    21  
    22  	out := collectRanked(feasible)
    23  	if len(out) != len(nodes) {
    24  		t.Fatalf("bad: %v", out)
    25  	}
    26  }
    27  
    28  func TestBinPackIterator_NoExistingAlloc(t *testing.T) {
    29  	_, ctx := testContext(t)
    30  	nodes := []*RankedNode{
    31  		{
    32  			Node: &structs.Node{
    33  				// Perfect fit
    34  				Resources: &structs.Resources{
    35  					CPU:      2048,
    36  					MemoryMB: 2048,
    37  				},
    38  				Reserved: &structs.Resources{
    39  					CPU:      1024,
    40  					MemoryMB: 1024,
    41  				},
    42  			},
    43  		},
    44  		{
    45  			Node: &structs.Node{
    46  				// Overloaded
    47  				Resources: &structs.Resources{
    48  					CPU:      1024,
    49  					MemoryMB: 1024,
    50  				},
    51  				Reserved: &structs.Resources{
    52  					CPU:      512,
    53  					MemoryMB: 512,
    54  				},
    55  			},
    56  		},
    57  		{
    58  			Node: &structs.Node{
    59  				// 50% fit
    60  				Resources: &structs.Resources{
    61  					CPU:      4096,
    62  					MemoryMB: 4096,
    63  				},
    64  				Reserved: &structs.Resources{
    65  					CPU:      1024,
    66  					MemoryMB: 1024,
    67  				},
    68  			},
    69  		},
    70  	}
    71  	static := NewStaticRankIterator(ctx, nodes)
    72  
    73  	taskGroup := &structs.TaskGroup{
    74  		EphemeralDisk: &structs.EphemeralDisk{},
    75  		Tasks: []*structs.Task{
    76  			{
    77  				Name: "web",
    78  				Resources: &structs.Resources{
    79  					CPU:      1024,
    80  					MemoryMB: 1024,
    81  				},
    82  			},
    83  		},
    84  	}
    85  	binp := NewBinPackIterator(ctx, static, false, 0)
    86  	binp.SetTaskGroup(taskGroup)
    87  
    88  	scoreNorm := NewScoreNormalizationIterator(ctx, binp)
    89  
    90  	out := collectRanked(scoreNorm)
    91  	if len(out) != 2 {
    92  		t.Fatalf("Bad: %v", out)
    93  	}
    94  	if out[0] != nodes[0] || out[1] != nodes[2] {
    95  		t.Fatalf("Bad: %v", out)
    96  	}
    97  
    98  	if out[0].FinalScore != 1.0 {
    99  		t.Fatalf("Bad Score: %v", out[0].FinalScore)
   100  	}
   101  	if out[1].FinalScore < 0.75 || out[1].FinalScore > 0.95 {
   102  		t.Fatalf("Bad Score: %v", out[1].FinalScore)
   103  	}
   104  }
   105  
   106  func TestBinPackIterator_PlannedAlloc(t *testing.T) {
   107  	_, ctx := testContext(t)
   108  	nodes := []*RankedNode{
   109  		{
   110  			Node: &structs.Node{
   111  				// Perfect fit
   112  				ID: uuid.Generate(),
   113  				Resources: &structs.Resources{
   114  					CPU:      2048,
   115  					MemoryMB: 2048,
   116  				},
   117  			},
   118  		},
   119  		{
   120  			Node: &structs.Node{
   121  				// Perfect fit
   122  				ID: uuid.Generate(),
   123  				Resources: &structs.Resources{
   124  					CPU:      2048,
   125  					MemoryMB: 2048,
   126  				},
   127  			},
   128  		},
   129  	}
   130  	static := NewStaticRankIterator(ctx, nodes)
   131  
   132  	// Add a planned alloc to node1 that fills it
   133  	plan := ctx.Plan()
   134  	plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{
   135  		{
   136  			Resources: &structs.Resources{
   137  				CPU:      2048,
   138  				MemoryMB: 2048,
   139  			},
   140  		},
   141  	}
   142  
   143  	// Add a planned alloc to node2 that half fills it
   144  	plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{
   145  		{
   146  			Resources: &structs.Resources{
   147  				CPU:      1024,
   148  				MemoryMB: 1024,
   149  			},
   150  		},
   151  	}
   152  
   153  	taskGroup := &structs.TaskGroup{
   154  		EphemeralDisk: &structs.EphemeralDisk{},
   155  		Tasks: []*structs.Task{
   156  			{
   157  				Name: "web",
   158  				Resources: &structs.Resources{
   159  					CPU:      1024,
   160  					MemoryMB: 1024,
   161  				},
   162  			},
   163  		},
   164  	}
   165  
   166  	binp := NewBinPackIterator(ctx, static, false, 0)
   167  	binp.SetTaskGroup(taskGroup)
   168  
   169  	scoreNorm := NewScoreNormalizationIterator(ctx, binp)
   170  
   171  	out := collectRanked(scoreNorm)
   172  	if len(out) != 1 {
   173  		t.Fatalf("Bad: %#v", out)
   174  	}
   175  	if out[0] != nodes[1] {
   176  		t.Fatalf("Bad Score: %v", out)
   177  	}
   178  
   179  	if out[0].FinalScore != 1.0 {
   180  		t.Fatalf("Bad Score: %v", out[0].FinalScore)
   181  	}
   182  }
   183  
   184  func TestBinPackIterator_ExistingAlloc(t *testing.T) {
   185  	state, ctx := testContext(t)
   186  	nodes := []*RankedNode{
   187  		{
   188  			Node: &structs.Node{
   189  				// Perfect fit
   190  				ID: uuid.Generate(),
   191  				Resources: &structs.Resources{
   192  					CPU:      2048,
   193  					MemoryMB: 2048,
   194  				},
   195  			},
   196  		},
   197  		{
   198  			Node: &structs.Node{
   199  				// Perfect fit
   200  				ID: uuid.Generate(),
   201  				Resources: &structs.Resources{
   202  					CPU:      2048,
   203  					MemoryMB: 2048,
   204  				},
   205  			},
   206  		},
   207  	}
   208  	static := NewStaticRankIterator(ctx, nodes)
   209  
   210  	// Add existing allocations
   211  	j1, j2 := mock.Job(), mock.Job()
   212  	alloc1 := &structs.Allocation{
   213  		Namespace: structs.DefaultNamespace,
   214  		ID:        uuid.Generate(),
   215  		EvalID:    uuid.Generate(),
   216  		NodeID:    nodes[0].Node.ID,
   217  		JobID:     j1.ID,
   218  		Job:       j1,
   219  		Resources: &structs.Resources{
   220  			CPU:      2048,
   221  			MemoryMB: 2048,
   222  		},
   223  		DesiredStatus: structs.AllocDesiredStatusRun,
   224  		ClientStatus:  structs.AllocClientStatusPending,
   225  		TaskGroup:     "web",
   226  	}
   227  	alloc2 := &structs.Allocation{
   228  		Namespace: structs.DefaultNamespace,
   229  		ID:        uuid.Generate(),
   230  		EvalID:    uuid.Generate(),
   231  		NodeID:    nodes[1].Node.ID,
   232  		JobID:     j2.ID,
   233  		Job:       j2,
   234  		Resources: &structs.Resources{
   235  			CPU:      1024,
   236  			MemoryMB: 1024,
   237  		},
   238  		DesiredStatus: structs.AllocDesiredStatusRun,
   239  		ClientStatus:  structs.AllocClientStatusPending,
   240  		TaskGroup:     "web",
   241  	}
   242  	noErr(t, state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID)))
   243  	noErr(t, state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID)))
   244  	noErr(t, state.UpsertAllocs(1000, []*structs.Allocation{alloc1, alloc2}))
   245  
   246  	taskGroup := &structs.TaskGroup{
   247  		EphemeralDisk: &structs.EphemeralDisk{},
   248  		Tasks: []*structs.Task{
   249  			{
   250  				Name: "web",
   251  				Resources: &structs.Resources{
   252  					CPU:      1024,
   253  					MemoryMB: 1024,
   254  				},
   255  			},
   256  		},
   257  	}
   258  	binp := NewBinPackIterator(ctx, static, false, 0)
   259  	binp.SetTaskGroup(taskGroup)
   260  
   261  	scoreNorm := NewScoreNormalizationIterator(ctx, binp)
   262  
   263  	out := collectRanked(scoreNorm)
   264  	if len(out) != 1 {
   265  		t.Fatalf("Bad: %#v", out)
   266  	}
   267  	if out[0] != nodes[1] {
   268  		t.Fatalf("Bad: %v", out)
   269  	}
   270  	if out[0].FinalScore != 1.0 {
   271  		t.Fatalf("Bad Score: %v", out[0].FinalScore)
   272  	}
   273  }
   274  
   275  func TestBinPackIterator_ExistingAlloc_PlannedEvict(t *testing.T) {
   276  	state, ctx := testContext(t)
   277  	nodes := []*RankedNode{
   278  		{
   279  			Node: &structs.Node{
   280  				// Perfect fit
   281  				ID: uuid.Generate(),
   282  				Resources: &structs.Resources{
   283  					CPU:      2048,
   284  					MemoryMB: 2048,
   285  				},
   286  			},
   287  		},
   288  		{
   289  			Node: &structs.Node{
   290  				// Perfect fit
   291  				ID: uuid.Generate(),
   292  				Resources: &structs.Resources{
   293  					CPU:      2048,
   294  					MemoryMB: 2048,
   295  				},
   296  			},
   297  		},
   298  	}
   299  	static := NewStaticRankIterator(ctx, nodes)
   300  
   301  	// Add existing allocations
   302  	j1, j2 := mock.Job(), mock.Job()
   303  	alloc1 := &structs.Allocation{
   304  		Namespace: structs.DefaultNamespace,
   305  		ID:        uuid.Generate(),
   306  		EvalID:    uuid.Generate(),
   307  		NodeID:    nodes[0].Node.ID,
   308  		JobID:     j1.ID,
   309  		Job:       j1,
   310  		Resources: &structs.Resources{
   311  			CPU:      2048,
   312  			MemoryMB: 2048,
   313  		},
   314  		DesiredStatus: structs.AllocDesiredStatusRun,
   315  		ClientStatus:  structs.AllocClientStatusPending,
   316  		TaskGroup:     "web",
   317  	}
   318  	alloc2 := &structs.Allocation{
   319  		Namespace: structs.DefaultNamespace,
   320  		ID:        uuid.Generate(),
   321  		EvalID:    uuid.Generate(),
   322  		NodeID:    nodes[1].Node.ID,
   323  		JobID:     j2.ID,
   324  		Job:       j2,
   325  		Resources: &structs.Resources{
   326  			CPU:      1024,
   327  			MemoryMB: 1024,
   328  		},
   329  		DesiredStatus: structs.AllocDesiredStatusRun,
   330  		ClientStatus:  structs.AllocClientStatusPending,
   331  		TaskGroup:     "web",
   332  	}
   333  	noErr(t, state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID)))
   334  	noErr(t, state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID)))
   335  	noErr(t, state.UpsertAllocs(1000, []*structs.Allocation{alloc1, alloc2}))
   336  
   337  	// Add a planned eviction to alloc1
   338  	plan := ctx.Plan()
   339  	plan.NodeUpdate[nodes[0].Node.ID] = []*structs.Allocation{alloc1}
   340  
   341  	taskGroup := &structs.TaskGroup{
   342  		EphemeralDisk: &structs.EphemeralDisk{},
   343  		Tasks: []*structs.Task{
   344  			{
   345  				Name: "web",
   346  				Resources: &structs.Resources{
   347  					CPU:      1024,
   348  					MemoryMB: 1024,
   349  				},
   350  			},
   351  		},
   352  	}
   353  
   354  	binp := NewBinPackIterator(ctx, static, false, 0)
   355  	binp.SetTaskGroup(taskGroup)
   356  
   357  	scoreNorm := NewScoreNormalizationIterator(ctx, binp)
   358  
   359  	out := collectRanked(scoreNorm)
   360  	if len(out) != 2 {
   361  		t.Fatalf("Bad: %#v", out)
   362  	}
   363  	if out[0] != nodes[0] || out[1] != nodes[1] {
   364  		t.Fatalf("Bad: %v", out)
   365  	}
   366  	if out[0].FinalScore < 0.50 || out[0].FinalScore > 0.95 {
   367  		t.Fatalf("Bad Score: %v", out[0].FinalScore)
   368  	}
   369  	if out[1].FinalScore != 1 {
   370  		t.Fatalf("Bad Score: %v", out[1].FinalScore)
   371  	}
   372  }
   373  
   374  func TestJobAntiAffinity_PlannedAlloc(t *testing.T) {
   375  	_, ctx := testContext(t)
   376  	nodes := []*RankedNode{
   377  		{
   378  			Node: &structs.Node{
   379  				ID: uuid.Generate(),
   380  			},
   381  		},
   382  		{
   383  			Node: &structs.Node{
   384  				ID: uuid.Generate(),
   385  			},
   386  		},
   387  	}
   388  	static := NewStaticRankIterator(ctx, nodes)
   389  
   390  	job := mock.Job()
   391  	job.ID = "foo"
   392  	tg := job.TaskGroups[0]
   393  	tg.Count = 4
   394  
   395  	// Add a planned alloc to node1 that fills it
   396  	plan := ctx.Plan()
   397  	plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{
   398  		{
   399  			ID:        uuid.Generate(),
   400  			JobID:     "foo",
   401  			TaskGroup: tg.Name,
   402  		},
   403  		{
   404  			ID:        uuid.Generate(),
   405  			JobID:     "foo",
   406  			TaskGroup: tg.Name,
   407  		},
   408  	}
   409  
   410  	// Add a planned alloc to node2 that half fills it
   411  	plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{
   412  		{
   413  			JobID: "bar",
   414  		},
   415  	}
   416  
   417  	jobAntiAff := NewJobAntiAffinityIterator(ctx, static, "foo")
   418  	jobAntiAff.SetJob(job)
   419  	jobAntiAff.SetTaskGroup(tg)
   420  
   421  	scoreNorm := NewScoreNormalizationIterator(ctx, jobAntiAff)
   422  
   423  	out := collectRanked(scoreNorm)
   424  	if len(out) != 2 {
   425  		t.Fatalf("Bad: %#v", out)
   426  	}
   427  	if out[0] != nodes[0] {
   428  		t.Fatalf("Bad: %v", out)
   429  	}
   430  	// Score should be -(#collissions+1/desired_count) => -(3/4)
   431  	if out[0].FinalScore != -0.75 {
   432  		t.Fatalf("Bad Score: %#v", out[0].FinalScore)
   433  	}
   434  
   435  	if out[1] != nodes[1] {
   436  		t.Fatalf("Bad: %v", out)
   437  	}
   438  	if out[1].FinalScore != 0.0 {
   439  		t.Fatalf("Bad Score: %v", out[1].FinalScore)
   440  	}
   441  }
   442  
   443  func collectRanked(iter RankIterator) (out []*RankedNode) {
   444  	for {
   445  		next := iter.Next()
   446  		if next == nil {
   447  			break
   448  		}
   449  		out = append(out, next)
   450  	}
   451  	return
   452  }
   453  
   454  func TestNodeAntiAffinity_PenaltyNodes(t *testing.T) {
   455  	_, ctx := testContext(t)
   456  	node1 := &structs.Node{
   457  		ID: uuid.Generate(),
   458  	}
   459  	node2 := &structs.Node{
   460  		ID: uuid.Generate(),
   461  	}
   462  
   463  	nodes := []*RankedNode{
   464  		{
   465  			Node: node1,
   466  		},
   467  		{
   468  			Node: node2,
   469  		},
   470  	}
   471  	static := NewStaticRankIterator(ctx, nodes)
   472  
   473  	nodeAntiAffIter := NewNodeReschedulingPenaltyIterator(ctx, static)
   474  	nodeAntiAffIter.SetPenaltyNodes(map[string]struct{}{node1.ID: {}})
   475  
   476  	scoreNorm := NewScoreNormalizationIterator(ctx, nodeAntiAffIter)
   477  
   478  	out := collectRanked(scoreNorm)
   479  
   480  	require := require.New(t)
   481  	require.Equal(2, len(out))
   482  	require.Equal(node1.ID, out[0].Node.ID)
   483  	require.Equal(-1.0, out[0].FinalScore)
   484  
   485  	require.Equal(node2.ID, out[1].Node.ID)
   486  	require.Equal(0.0, out[1].FinalScore)
   487  
   488  }
   489  
   490  func TestScoreNormalizationIterator(t *testing.T) {
   491  	// Test normalized scores when there is more than one scorer
   492  	_, ctx := testContext(t)
   493  	nodes := []*RankedNode{
   494  		{
   495  			Node: &structs.Node{
   496  				ID: uuid.Generate(),
   497  			},
   498  		},
   499  		{
   500  			Node: &structs.Node{
   501  				ID: uuid.Generate(),
   502  			},
   503  		},
   504  	}
   505  	static := NewStaticRankIterator(ctx, nodes)
   506  
   507  	job := mock.Job()
   508  	job.ID = "foo"
   509  	tg := job.TaskGroups[0]
   510  	tg.Count = 4
   511  
   512  	// Add a planned alloc to node1 that fills it
   513  	plan := ctx.Plan()
   514  	plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{
   515  		{
   516  			ID:        uuid.Generate(),
   517  			JobID:     "foo",
   518  			TaskGroup: tg.Name,
   519  		},
   520  		{
   521  			ID:        uuid.Generate(),
   522  			JobID:     "foo",
   523  			TaskGroup: tg.Name,
   524  		},
   525  	}
   526  
   527  	// Add a planned alloc to node2 that half fills it
   528  	plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{
   529  		{
   530  			JobID: "bar",
   531  		},
   532  	}
   533  
   534  	jobAntiAff := NewJobAntiAffinityIterator(ctx, static, "foo")
   535  	jobAntiAff.SetJob(job)
   536  	jobAntiAff.SetTaskGroup(tg)
   537  
   538  	nodeReschedulePenaltyIter := NewNodeReschedulingPenaltyIterator(ctx, jobAntiAff)
   539  	nodeReschedulePenaltyIter.SetPenaltyNodes(map[string]struct{}{nodes[0].Node.ID: {}})
   540  
   541  	scoreNorm := NewScoreNormalizationIterator(ctx, nodeReschedulePenaltyIter)
   542  
   543  	out := collectRanked(scoreNorm)
   544  	require := require.New(t)
   545  
   546  	require.Equal(2, len(out))
   547  	require.Equal(out[0], nodes[0])
   548  	// Score should be averaged between both scorers
   549  	// -0.75 from job anti affinity and -1 from node rescheduling penalty
   550  	require.Equal(-0.875, out[0].FinalScore)
   551  	require.Equal(out[1], nodes[1])
   552  	require.Equal(out[1].FinalScore, 0.0)
   553  }
   554  
   555  func TestNodeAffinityIterator(t *testing.T) {
   556  	_, ctx := testContext(t)
   557  	nodes := []*RankedNode{
   558  		{Node: mock.Node()},
   559  		{Node: mock.Node()},
   560  		{Node: mock.Node()},
   561  		{Node: mock.Node()},
   562  	}
   563  
   564  	nodes[0].Node.Attributes["kernel.version"] = "4.9"
   565  	nodes[1].Node.Datacenter = "dc2"
   566  	nodes[2].Node.Datacenter = "dc2"
   567  	nodes[2].Node.NodeClass = "large"
   568  
   569  	affinities := []*structs.Affinity{
   570  		{
   571  			Operand: "=",
   572  			LTarget: "${node.datacenter}",
   573  			RTarget: "dc1",
   574  			Weight:  200,
   575  		},
   576  		{
   577  			Operand: "=",
   578  			LTarget: "${node.datacenter}",
   579  			RTarget: "dc2",
   580  			Weight:  -100,
   581  		},
   582  		{
   583  			Operand: "version",
   584  			LTarget: "${attr.kernel.version}",
   585  			RTarget: ">4.0",
   586  			Weight:  50,
   587  		},
   588  		{
   589  			Operand: "is",
   590  			LTarget: "${node.class}",
   591  			RTarget: "large",
   592  			Weight:  50,
   593  		},
   594  	}
   595  
   596  	static := NewStaticRankIterator(ctx, nodes)
   597  
   598  	job := mock.Job()
   599  	job.ID = "foo"
   600  	tg := job.TaskGroups[0]
   601  	tg.Affinities = affinities
   602  
   603  	nodeAffinity := NewNodeAffinityIterator(ctx, static)
   604  	nodeAffinity.SetTaskGroup(tg)
   605  
   606  	scoreNorm := NewScoreNormalizationIterator(ctx, nodeAffinity)
   607  
   608  	out := collectRanked(scoreNorm)
   609  	expectedScores := make(map[string]float64)
   610  	// Total weight = 400
   611  	// Node 0 matches two affinities(dc and kernel version), total weight =250
   612  	expectedScores[nodes[0].Node.ID] = 0.625
   613  
   614  	// Node 1 matches an anti affinity, weight = -100
   615  	expectedScores[nodes[1].Node.ID] = -0.25
   616  
   617  	// Node 2 matches one affinity(node class) with weight 50
   618  	expectedScores[nodes[2].Node.ID] = -0.125
   619  
   620  	// Node 3 matches one affinity (dc) with weight = 200
   621  	expectedScores[nodes[3].Node.ID] = 0.5
   622  
   623  	require := require.New(t)
   624  	for _, n := range out {
   625  		require.Equal(expectedScores[n.Node.ID], n.FinalScore)
   626  	}
   627  
   628  }