volcano.sh/volcano@v1.9.0/pkg/scheduler/api/job_info_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package api
    18  
    19  import (
    20  	"reflect"
    21  	"testing"
    22  
    23  	"github.com/stretchr/testify/assert"
    24  	v1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/api/resource"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  
    28  	"k8s.io/apimachinery/pkg/types"
    29  	"volcano.sh/apis/pkg/apis/scheduling"
    30  	schedulingv2 "volcano.sh/apis/pkg/apis/scheduling/v1beta1"
    31  )
    32  
    33  func jobInfoEqual(l, r *JobInfo) bool {
    34  	return reflect.DeepEqual(l, r)
    35  }
    36  
    37  func TestAddTaskInfo(t *testing.T) {
    38  	// case1
    39  	case01UID := JobID("uid")
    40  	case01Ns := "c1"
    41  	case01Owner := buildOwnerReference("uid")
    42  
    43  	case01Pod1 := buildPod(case01Ns, "p1", "", v1.PodPending, BuildResourceList("1000m", "1G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
    44  	case01Task1 := NewTaskInfo(case01Pod1)
    45  	case01Pod2 := buildPod(case01Ns, "p2", "n1", v1.PodRunning, BuildResourceList("2000m", "2G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
    46  	case01Task2 := NewTaskInfo(case01Pod2)
    47  	case01Pod3 := buildPod(case01Ns, "p3", "n1", v1.PodPending, BuildResourceList("1000m", "1G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
    48  	case01Task3 := NewTaskInfo(case01Pod3)
    49  	case01Pod4 := buildPod(case01Ns, "p4", "n1", v1.PodPending, BuildResourceList("1000m", "1G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
    50  	case01Task4 := NewTaskInfo(case01Pod4)
    51  
    52  	tests := []struct {
    53  		name     string
    54  		uid      JobID
    55  		pods     []*v1.Pod
    56  		expected *JobInfo
    57  	}{
    58  		{
    59  			name: "add 1 pending owner pod, 1 running owner pod",
    60  			uid:  case01UID,
    61  			pods: []*v1.Pod{case01Pod1, case01Pod2, case01Pod3, case01Pod4},
    62  			expected: &JobInfo{
    63  				UID:          case01UID,
    64  				Allocated:    buildResource("4000m", "4G", map[string]string{"pods": "3"}, 0),
    65  				TotalRequest: buildResource("5000m", "5G", map[string]string{"pods": "4"}, 0),
    66  				Tasks: tasksMap{
    67  					case01Task1.UID: case01Task1,
    68  					case01Task2.UID: case01Task2,
    69  					case01Task3.UID: case01Task3,
    70  					case01Task4.UID: case01Task4,
    71  				},
    72  				TaskStatusIndex: map[TaskStatus]tasksMap{
    73  					Running: {
    74  						case01Task2.UID: case01Task2,
    75  					},
    76  					Pending: {
    77  						case01Task1.UID: case01Task1,
    78  					},
    79  					Bound: {
    80  						case01Task3.UID: case01Task3,
    81  						case01Task4.UID: case01Task4,
    82  					},
    83  				},
    84  				NodesFitErrors:   make(map[TaskID]*FitErrors),
    85  				TaskMinAvailable: make(map[TaskID]int32),
    86  				Budget:           &DisruptionBudget{},
    87  			},
    88  		},
    89  	}
    90  
    91  	for i, test := range tests {
    92  		ps := NewJobInfo(test.uid)
    93  		ps.Budget = &DisruptionBudget{}
    94  
    95  		for _, pod := range test.pods {
    96  			pi := NewTaskInfo(pod)
    97  			ps.AddTaskInfo(pi)
    98  		}
    99  		if !jobInfoEqual(ps, test.expected) {
   100  			t.Errorf("podset info %d: \n expected: %v, \n got: %v \n",
   101  				i, test.expected, ps)
   102  		}
   103  	}
   104  }
   105  
   106  func TestDeleteTaskInfo(t *testing.T) {
   107  	// case1
   108  	case01UID := JobID("owner1")
   109  	case01Ns := "c1"
   110  	case01Owner := buildOwnerReference(string(case01UID))
   111  	case01Pod1 := buildPod(case01Ns, "p1", "", v1.PodPending, BuildResourceList("1000m", "1G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
   112  	case01Task1 := NewTaskInfo(case01Pod1)
   113  	case01Pod2 := buildPod(case01Ns, "p2", "n1", v1.PodRunning, BuildResourceList("2000m", "2G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
   114  	case01Pod3 := buildPod(case01Ns, "p3", "n1", v1.PodRunning, BuildResourceList("3000m", "3G"), []metav1.OwnerReference{case01Owner}, make(map[string]string))
   115  	case01Task3 := NewTaskInfo(case01Pod3)
   116  
   117  	// case2
   118  	case02UID := JobID("owner2")
   119  	case02Ns := "c2"
   120  	case02Owner := buildOwnerReference(string(case02UID))
   121  	case02Pod1 := buildPod(case02Ns, "p1", "", v1.PodPending, BuildResourceList("1000m", "1G"), []metav1.OwnerReference{case02Owner}, make(map[string]string))
   122  	case02Task1 := NewTaskInfo(case02Pod1)
   123  	case02Pod2 := buildPod(case02Ns, "p2", "n1", v1.PodPending, BuildResourceList("2000m", "2G"), []metav1.OwnerReference{case02Owner}, make(map[string]string))
   124  	case02Pod3 := buildPod(case02Ns, "p3", "n1", v1.PodRunning, BuildResourceList("3000m", "3G"), []metav1.OwnerReference{case02Owner}, make(map[string]string))
   125  	case02Task3 := NewTaskInfo(case02Pod3)
   126  
   127  	tests := []struct {
   128  		name     string
   129  		uid      JobID
   130  		pods     []*v1.Pod
   131  		rmPods   []*v1.Pod
   132  		expected *JobInfo
   133  	}{
   134  		{
   135  			name:   "add 1 pending owner pod, 2 running owner pod, remove 1 running owner pod",
   136  			uid:    case01UID,
   137  			pods:   []*v1.Pod{case01Pod1, case01Pod2, case01Pod3},
   138  			rmPods: []*v1.Pod{case01Pod2},
   139  			expected: &JobInfo{
   140  				Allocated:    buildResource("3000m", "3G", map[string]string{"pods": "1"}, 0),
   141  				TotalRequest: buildResource("4000m", "4G", map[string]string{"pods": "2"}, 0),
   142  				UID:          case01UID,
   143  				Tasks: tasksMap{
   144  					case01Task1.UID: case01Task1,
   145  					case01Task3.UID: case01Task3,
   146  				},
   147  				TaskStatusIndex: map[TaskStatus]tasksMap{
   148  					Pending: {case01Task1.UID: case01Task1},
   149  					Running: {case01Task3.UID: case01Task3},
   150  				},
   151  				NodesFitErrors:   make(map[TaskID]*FitErrors),
   152  				TaskMinAvailable: make(map[TaskID]int32),
   153  				Budget:           &DisruptionBudget{},
   154  			},
   155  		},
   156  		{
   157  			name:   "add 2 pending owner pod, 1 running owner pod, remove 1 pending owner pod",
   158  			uid:    case02UID,
   159  			pods:   []*v1.Pod{case02Pod1, case02Pod2, case02Pod3},
   160  			rmPods: []*v1.Pod{case02Pod2},
   161  			expected: &JobInfo{
   162  				Allocated:    buildResource("3000m", "3G", map[string]string{"pods": "1"}, 0),
   163  				TotalRequest: buildResource("4000m", "4G", map[string]string{"pods": "2"}, 0),
   164  				UID:          case02UID,
   165  				Tasks: tasksMap{
   166  					case02Task1.UID: case02Task1,
   167  					case02Task3.UID: case02Task3,
   168  				},
   169  				TaskStatusIndex: map[TaskStatus]tasksMap{
   170  					Pending: {
   171  						case02Task1.UID: case02Task1,
   172  					},
   173  					Running: {
   174  						case02Task3.UID: case02Task3,
   175  					},
   176  				},
   177  				NodesFitErrors:   make(map[TaskID]*FitErrors),
   178  				TaskMinAvailable: make(map[TaskID]int32),
   179  				Budget:           &DisruptionBudget{},
   180  			},
   181  		},
   182  	}
   183  
   184  	for i, test := range tests {
   185  		ps := NewJobInfo(test.uid)
   186  		ps.Budget = &DisruptionBudget{}
   187  
   188  		for _, pod := range test.pods {
   189  			pi := NewTaskInfo(pod)
   190  			ps.AddTaskInfo(pi)
   191  		}
   192  
   193  		for _, pod := range test.rmPods {
   194  			pi := NewTaskInfo(pod)
   195  			ps.DeleteTaskInfo(pi)
   196  		}
   197  
   198  		if !jobInfoEqual(ps, test.expected) {
   199  			t.Errorf("podset info %d: \n expected: %v, \n got: %v \n",
   200  				i, test.expected, ps)
   201  		}
   202  	}
   203  }
   204  
   205  func TestTaskSchedulingReason(t *testing.T) {
   206  	t1 := buildPod("ns1", "task-1", "", v1.PodPending, BuildResourceList("1", "1G"), nil, make(map[string]string))
   207  	t2 := buildPod("ns1", "task-2", "", v1.PodPending, BuildResourceList("1", "1G"), nil, make(map[string]string))
   208  	t3 := buildPod("ns1", "task-3", "node1", v1.PodPending, BuildResourceList("1", "1G"), nil, make(map[string]string))
   209  	t4 := buildPod("ns1", "task-4", "node2", v1.PodPending, BuildResourceList("1", "1G"), nil, make(map[string]string))
   210  	t5 := buildPod("ns1", "task-5", "node3", v1.PodPending, BuildResourceList("1", "1G"), nil, make(map[string]string))
   211  	t6 := buildPod("ns1", "task-6", "", v1.PodPending, BuildResourceList("1", "1G"), nil, make(map[string]string))
   212  
   213  	tests := []struct {
   214  		desc     string
   215  		pods     []*v1.Pod
   216  		jobid    JobID
   217  		nodefes  map[TaskID]*FitErrors
   218  		expected map[types.UID]string
   219  	}{
   220  		{
   221  			desc:  "task3 ~ 5 are schedulable",
   222  			pods:  []*v1.Pod{t1, t2, t3, t4, t5, t6},
   223  			jobid: JobID("case1"),
   224  			nodefes: map[TaskID]*FitErrors{
   225  				TaskID(t6.UID): {
   226  					nodes: map[string]*FitError{
   227  						"node1": {Reasons: []string{NodePodNumberExceeded}},
   228  						"node2": {Reasons: []string{NodeResourceFitFailed}},
   229  						"node3": {Reasons: []string{NodeResourceFitFailed}},
   230  					},
   231  				},
   232  			},
   233  			expected: map[types.UID]string{
   234  				"pg":   "pod group is not ready, 6 Pending, 6 minAvailable; Pending: 3 Schedulable, 3 Unschedulable",
   235  				t1.UID: "pod group is not ready, 6 Pending, 6 minAvailable; Pending: 3 Schedulable, 3 Unschedulable",
   236  				t2.UID: "pod group is not ready, 6 Pending, 6 minAvailable; Pending: 3 Schedulable, 3 Unschedulable",
   237  				t3.UID: "Pod ns1/task-3 can possibly be assigned to node1",
   238  				t4.UID: "Pod ns1/task-4 can possibly be assigned to node2",
   239  				t5.UID: "Pod ns1/task-5 can possibly be assigned to node3",
   240  				t6.UID: "0/3 nodes are unavailable: 1 node(s) pod number exceeded, 2 node(s) resource fit failed.",
   241  			},
   242  		},
   243  	}
   244  
   245  	for i, test := range tests {
   246  		job := NewJobInfo(test.jobid)
   247  		pg := scheduling.PodGroup{
   248  			ObjectMeta: metav1.ObjectMeta{
   249  				Namespace: "ns1",
   250  				Name:      "pg1",
   251  			},
   252  			Spec: scheduling.PodGroupSpec{
   253  				MinMember: int32(len(test.pods)),
   254  			},
   255  		}
   256  		for _, pod := range test.pods {
   257  			// set pod group
   258  			pod.Annotations = map[string]string{
   259  				schedulingv2.KubeGroupNameAnnotationKey: pg.Name,
   260  			}
   261  
   262  			// add TaskInfo
   263  			ti := NewTaskInfo(pod)
   264  			job.AddTaskInfo(ti)
   265  
   266  			// pod is schedulable
   267  			if len(pod.Spec.NodeName) > 0 {
   268  				ti.LastTransaction = &TransactionContext{
   269  					NodeName: pod.Spec.NodeName,
   270  					Status:   Allocated,
   271  				}
   272  			}
   273  		}
   274  		// complete job
   275  		job.SetPodGroup(&PodGroup{PodGroup: pg})
   276  		job.NodesFitErrors = test.nodefes
   277  		job.TaskStatusIndex = map[TaskStatus]tasksMap{Pending: {}}
   278  		for _, task := range job.Tasks {
   279  			task.Status = Pending
   280  			job.TaskStatusIndex[Pending][task.UID] = task
   281  		}
   282  		job.JobFitErrors = job.FitError()
   283  
   284  		// assert
   285  		for uid, exp := range test.expected {
   286  			msg := job.JobFitErrors
   287  			if uid != "pg" {
   288  				_, msg = job.TaskSchedulingReason(TaskID(uid))
   289  			}
   290  			t.Logf("case #%d, task %v, result: %s", i, uid, msg)
   291  			if msg != exp {
   292  				t.Errorf("[x] case #%d, task %v, expected: %s, got: %s", i, uid, exp, msg)
   293  			}
   294  		}
   295  	}
   296  }
   297  
   298  func TestJobInfo(t *testing.T) {
   299  	newTaskFunc := func(uid, jobUid types.UID, status TaskStatus, resources *Resource) *TaskInfo {
   300  		isBestEffort := resources.IsEmpty()
   301  		return &TaskInfo{
   302  			UID:  TaskID(uid),
   303  			Job:  JobID(jobUid),
   304  			Name: string(uid),
   305  			TransactionContext: TransactionContext{
   306  				Status: status,
   307  			},
   308  			Resreq:     resources,
   309  			InitResreq: resources,
   310  			BestEffort: isBestEffort,
   311  			NumaInfo: &TopologyInfo{
   312  				ResMap: map[int]v1.ResourceList{},
   313  			},
   314  		}
   315  	}
   316  
   317  	testCases := []struct {
   318  		name                             string
   319  		jobUID                           JobID
   320  		jobMinAvailable                  int32
   321  		tasks                            []*TaskInfo
   322  		expectedPendingBestEffortTaskNum int32
   323  		expectedIsReady                  bool
   324  		expectedIsPipelined              bool
   325  		expectedIsStarving               bool
   326  	}{
   327  		{
   328  			name:            "starving job",
   329  			jobUID:          "job-1",
   330  			jobMinAvailable: 5,
   331  			tasks: []*TaskInfo{
   332  				newTaskFunc("pending-besteffort-task-1", "job-1", Pending, EmptyResource()),
   333  				newTaskFunc("pipelined-besteffort-task-1", "job-1", Pipelined, EmptyResource()),
   334  				newTaskFunc("running-besteffort-task-1", "job-1", Running, EmptyResource()),
   335  				newTaskFunc("pending-unbesteffort-task-1", "job-1", Pending, NewResource(v1.ResourceList{"cpu": resource.MustParse("100m")})),
   336  				newTaskFunc("pipelined-unbesteffort-task-1", "job-1", Pipelined, NewResource(v1.ResourceList{"cpu": resource.MustParse("100m")})),
   337  				newTaskFunc("running-unbesteffort-task-1", "job-1", Running, NewResource(v1.ResourceList{"cpu": resource.MustParse("100m")})),
   338  			},
   339  			expectedPendingBestEffortTaskNum: 1,
   340  			expectedIsReady:                  false,
   341  			expectedIsPipelined:              true,
   342  			expectedIsStarving:               true,
   343  		},
   344  
   345  		{
   346  			name:            "ready job",
   347  			jobUID:          "job-1",
   348  			jobMinAvailable: 3,
   349  			tasks: []*TaskInfo{
   350  				newTaskFunc("pending-besteffort-task-1", "job-1", Pending, EmptyResource()),
   351  				newTaskFunc("pipelined-besteffort-task-1", "job-1", Pipelined, EmptyResource()),
   352  				newTaskFunc("running-besteffort-task-1", "job-1", Running, EmptyResource()),
   353  				newTaskFunc("pending-unbesteffort-task-1", "job-1", Pending, NewResource(v1.ResourceList{"cpu": resource.MustParse("100m")})),
   354  				newTaskFunc("pipelined-unbesteffort-task-1", "job-1", Pipelined, NewResource(v1.ResourceList{"cpu": resource.MustParse("100m")})),
   355  				newTaskFunc("running-unbesteffort-task-1", "job-1", Running, NewResource(v1.ResourceList{"cpu": resource.MustParse("100m")})),
   356  			},
   357  			expectedPendingBestEffortTaskNum: 1,
   358  			expectedIsReady:                  true,
   359  			expectedIsPipelined:              true,
   360  			expectedIsStarving:               false,
   361  		},
   362  	}
   363  
   364  	for _, tc := range testCases {
   365  		jobInfo := NewJobInfo(tc.jobUID, tc.tasks...)
   366  		jobInfo.MinAvailable = tc.jobMinAvailable
   367  		actualPendingBestEffortTaskNum := jobInfo.PendingBestEffortTaskNum()
   368  		actualIsReady := jobInfo.IsReady()
   369  		actualIsPipelined := jobInfo.IsPipelined()
   370  		actualIsStarving := jobInfo.IsStarving()
   371  
   372  		if !assert.Equal(t, actualPendingBestEffortTaskNum, tc.expectedPendingBestEffortTaskNum) {
   373  			t.Errorf("unexpected PendingBestEffortTaskNum; name: %s, expected result: %v, actual result: %v", tc.name, tc.expectedPendingBestEffortTaskNum, actualPendingBestEffortTaskNum)
   374  		}
   375  		if !assert.Equal(t, actualIsReady, tc.expectedIsReady) {
   376  			t.Errorf("unexpected IsReady; name: %s, expected result: %v, actual result: %v", tc.name, tc.expectedIsReady, actualIsReady)
   377  		}
   378  		if !assert.Equal(t, actualIsPipelined, tc.expectedIsPipelined) {
   379  			t.Errorf("unexpected IsPipelined; name: %s, expected result: %v, actual result: %v", tc.name, tc.expectedIsPipelined, actualIsPipelined)
   380  		}
   381  		if !assert.Equal(t, actualIsStarving, tc.expectedIsStarving) {
   382  			t.Errorf("unexpected IsStarving; name: %s, expected result: %v, actual result: %v", tc.name, tc.expectedIsStarving, actualIsStarving)
   383  		}
   384  	}
   385  }