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 }