golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/pool/ledger_test.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build linux || darwin 6 7 package pool 8 9 import ( 10 "context" 11 "sort" 12 "testing" 13 "time" 14 15 "golang.org/x/build/internal/cloud" 16 "golang.org/x/build/internal/coordinator/pool/queue" 17 ) 18 19 func canceledContext() context.Context { 20 ctx, cancel := context.WithCancel(context.Background()) 21 cancel() 22 return ctx 23 } 24 25 func TestLedgerReserveResources(t *testing.T) { 26 testCases := []struct { 27 desc string 28 ctx context.Context 29 instName string 30 vmType string 31 instTypes []*cloud.InstanceType 32 cpuLimit int64 33 cpuUsed int64 34 wantErr bool 35 }{ 36 { 37 desc: "success", 38 ctx: context.Background(), 39 instName: "small-instance", 40 vmType: "aa.small", 41 instTypes: []*cloud.InstanceType{ 42 { 43 Type: "aa.small", 44 CPU: 5, 45 }, 46 }, 47 cpuLimit: 20, 48 cpuUsed: 5, 49 wantErr: false, 50 }, 51 { 52 desc: "cancelled-context", 53 ctx: canceledContext(), 54 instName: "small-instance", 55 vmType: "aa.small", 56 instTypes: []*cloud.InstanceType{ 57 { 58 Type: "aa.small", 59 CPU: 5, 60 }, 61 }, 62 cpuLimit: 20, 63 cpuUsed: 20, 64 wantErr: true, 65 }, 66 { 67 desc: "unknown-instance-type", 68 ctx: context.Background(), 69 instName: "small-instance", 70 vmType: "aa.small", 71 instTypes: []*cloud.InstanceType{}, 72 cpuLimit: 20, 73 cpuUsed: 5, 74 wantErr: true, 75 }, 76 { 77 desc: "instance-already-exists", 78 ctx: context.Background(), 79 instName: "large-instance", 80 vmType: "aa.small", 81 instTypes: []*cloud.InstanceType{ 82 { 83 Type: "aa.small", 84 CPU: 5, 85 }, 86 }, 87 cpuLimit: 20, 88 cpuUsed: 5, 89 wantErr: true, 90 }, 91 } 92 for _, tc := range testCases { 93 t.Run(tc.desc, func(t *testing.T) { 94 l := newLedger() 95 l.entries = map[string]*entry{ 96 "large-instance": {}, 97 } 98 l.SetCPULimit(tc.cpuLimit) 99 l.types = make(map[string]*cloud.InstanceType) 100 l.UpdateInstanceTypes(tc.instTypes) 101 gotErr := l.ReserveResources(tc.ctx, tc.instName, tc.vmType, new(queue.SchedItem)) 102 if (gotErr != nil) != tc.wantErr { 103 t.Errorf("ledger.ReserveResources(%+v, %s, %s) = %s; want error %t", tc.ctx, tc.instName, tc.vmType, gotErr, tc.wantErr) 104 } 105 }) 106 } 107 } 108 109 func TestLedgerReleaseResources(t *testing.T) { 110 testCases := []struct { 111 desc string 112 instName string 113 entry *entry 114 cpuUsed int64 115 a1Used int64 116 wantCPUUsed int64 117 wantErr bool 118 }{ 119 { 120 desc: "success", 121 instName: "inst-x", 122 entry: &entry{ 123 instanceName: "inst-x", 124 vCPUCount: 10, 125 }, 126 cpuUsed: 20, 127 a1Used: 0, 128 wantCPUUsed: 10, 129 wantErr: false, 130 }, 131 { 132 desc: "entry-not-found", 133 instName: "inst-x", 134 entry: &entry{ 135 instanceName: "inst-w", 136 vCPUCount: 10, 137 }, 138 cpuUsed: 20, 139 a1Used: 0, 140 wantCPUUsed: 20, 141 wantErr: true, 142 }, 143 } 144 for _, tc := range testCases { 145 t.Run(tc.desc, func(t *testing.T) { 146 l := newLedger() 147 l.cpuQueue.UpdateQuotas(int(tc.cpuUsed-tc.entry.vCPUCount), 20) 148 l.entries = map[string]*entry{ 149 tc.entry.instanceName: tc.entry, 150 } 151 item := l.cpuQueue.Enqueue(int(tc.entry.vCPUCount), new(queue.SchedItem)) 152 if err := item.Await(context.Background()); err != nil { 153 t.Fatalf("item.Await() = %q, wanted no error", err) 154 } 155 tc.entry.quota = item 156 gotErr := l.releaseResources(tc.instName) 157 if (gotErr != nil) != tc.wantErr { 158 t.Errorf("ledger.releaseResources(%s) = %s; want error %t", tc.instName, gotErr, tc.wantErr) 159 } 160 usage := l.cpuQueue.Quotas() 161 if int64(usage.Used) != tc.wantCPUUsed { 162 t.Errorf("ledger.cpuUsed = %d; wanted %d", usage.Used, tc.wantCPUUsed) 163 } 164 }) 165 } 166 } 167 168 func TestReserveResourcesEntries(t *testing.T) { 169 testCases := []struct { 170 desc string 171 numCPU int64 172 cpuLimit int64 173 cpuUsed int64 174 instName string 175 instType string 176 wantErr bool 177 wantCPUUsed int 178 wantA1Used int 179 }{ 180 { 181 desc: "reservation-success", 182 numCPU: 10, 183 cpuLimit: 10, 184 cpuUsed: 0, 185 instName: "chacha", 186 instType: "x.type", 187 wantErr: false, 188 wantCPUUsed: 10, 189 wantA1Used: 0, 190 }, 191 { 192 desc: "failed-to-reserve", 193 numCPU: 10, 194 cpuLimit: 5, 195 cpuUsed: 0, 196 instName: "pasa", 197 instType: "x.type", 198 wantErr: true, 199 wantCPUUsed: 0, 200 wantA1Used: 0, 201 }, 202 { 203 desc: "invalid-cpu-count", 204 numCPU: 0, 205 cpuLimit: 50, 206 cpuUsed: 20, 207 instName: "double", 208 instType: "x.type", 209 wantErr: true, 210 wantCPUUsed: 20, 211 wantA1Used: 0, 212 }, 213 } 214 for _, tc := range testCases { 215 t.Run(tc.desc, func(t *testing.T) { 216 l := newLedger() 217 l.types = make(map[string]*cloud.InstanceType) 218 l.UpdateInstanceTypes([]*cloud.InstanceType{{Type: tc.instType, CPU: tc.numCPU}}) 219 l.cpuQueue.UpdateQuotas(int(tc.cpuUsed), int(tc.cpuLimit)) 220 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 221 defer cancel() 222 err := l.ReserveResources(ctx, tc.instName, tc.instType, new(queue.SchedItem)) 223 if (err != nil) != tc.wantErr { 224 t.Errorf("ledger.allocateResources(%d) = %v, wantErr: %v", tc.numCPU, err, tc.wantErr) 225 } 226 usage := l.cpuQueue.Quotas() 227 if usage.Used != tc.wantCPUUsed { 228 t.Errorf("ledger.cpuUsed = %d; want %d", usage.Used, tc.wantCPUUsed) 229 } 230 if _, ok := l.entries[tc.instName]; !tc.wantErr && !ok { 231 t.Fatalf("ledger.entries[%s] = nil; want it to exist", tc.instName) 232 } 233 if e, _ := l.entries[tc.instName]; !tc.wantErr && e.vCPUCount != tc.numCPU { 234 t.Fatalf("ledger.entries[%s].vCPUCount = %d; want %d", tc.instName, e.vCPUCount, tc.numCPU) 235 } 236 }) 237 } 238 } 239 240 func TestLedgerUpdateReservation(t *testing.T) { 241 testCases := []struct { 242 desc string 243 instName string 244 instID string 245 entry *entry 246 wantErr bool 247 }{ 248 { 249 desc: "success", 250 instName: "inst-x", 251 instID: "id-foo-x", 252 entry: &entry{ 253 instanceName: "inst-x", 254 }, 255 wantErr: false, 256 }, 257 { 258 desc: "success", 259 instName: "inst-x", 260 instID: "id-foo-x", 261 entry: &entry{ 262 instanceName: "inst-w", 263 }, 264 wantErr: true, 265 }, 266 } 267 for _, tc := range testCases { 268 t.Run(tc.desc, func(t *testing.T) { 269 l := newLedger() 270 l.entries = map[string]*entry{ 271 tc.entry.instanceName: tc.entry, 272 } 273 if gotErr := l.UpdateReservation(tc.instName, tc.instID); (gotErr != nil) != tc.wantErr { 274 t.Errorf("ledger.updateReservation(%s, %s) = %s; want error %t", tc.instName, tc.instID, gotErr, tc.wantErr) 275 } 276 e, ok := l.entries[tc.instName] 277 if !tc.wantErr && !ok { 278 t.Fatalf("ledger.entries[%s] does not exist", tc.instName) 279 } 280 if !tc.wantErr && e.createdAt.IsZero() { 281 t.Errorf("ledger.entries[%s].createdAt = %s; time not set", tc.instName, e.createdAt) 282 } 283 }) 284 } 285 } 286 287 func TestLedgerRemove(t *testing.T) { 288 testCases := []struct { 289 desc string 290 instName string 291 entry *entry 292 cpuUsed int 293 wantCPUUsed int 294 wantErr bool 295 }{ 296 { 297 desc: "success", 298 instName: "inst-x", 299 entry: &entry{ 300 instanceName: "inst-x", 301 vCPUCount: 10, 302 }, 303 cpuUsed: 100, 304 wantCPUUsed: 90, 305 wantErr: false, 306 }, 307 { 308 desc: "entry-does-not-exist", 309 instName: "inst-x", 310 entry: &entry{ 311 instanceName: "inst-w", 312 vCPUCount: 10, 313 }, 314 cpuUsed: 100, 315 wantCPUUsed: 100, 316 wantErr: true, 317 }, 318 } 319 for _, tc := range testCases { 320 t.Run(tc.desc, func(t *testing.T) { 321 l := newLedger() 322 l.cpuQueue.UpdateQuotas(tc.cpuUsed-int(tc.entry.vCPUCount), 100) 323 l.entries = map[string]*entry{ 324 tc.entry.instanceName: tc.entry, 325 } 326 item := l.cpuQueue.Enqueue(int(tc.entry.vCPUCount), new(queue.SchedItem)) 327 if err := item.Await(context.Background()); err != nil { 328 t.Fatalf("item.Await() = %q, wanted no error", err) 329 } 330 tc.entry.quota = item 331 l.cpuQueue.UpdateQuotas(tc.cpuUsed, 20) 332 if gotErr := l.Remove(tc.instName); (gotErr != nil) != tc.wantErr { 333 t.Errorf("ledger.remove(%s) = %s; want error %t", tc.instName, gotErr, tc.wantErr) 334 } 335 if gotE, ok := l.entries[tc.instName]; ok { 336 t.Errorf("ledger.entries[%s] = %+v; want it not to exist", tc.instName, gotE) 337 } 338 usage := l.cpuQueue.Quotas() 339 if usage.Used != tc.wantCPUUsed { 340 t.Errorf("ledger.cpuUsed = %d; want %d", usage.Used, tc.wantCPUUsed) 341 } 342 }) 343 } 344 } 345 346 func TestLedgerSetCPULimit(t *testing.T) { 347 l := newLedger() 348 want := 300 349 l.SetCPULimit(int64(want)) 350 usage := l.cpuQueue.Quotas() 351 if usage.Limit != want { 352 t.Errorf("ledger.cpuLimit = %d; want %d", want, want) 353 } 354 } 355 356 func TestLedgerUpdateInstanceTypes(t *testing.T) { 357 testCases := []struct { 358 desc string 359 types []*cloud.InstanceType 360 }{ 361 {"no-type", []*cloud.InstanceType{}}, 362 {"single-type", []*cloud.InstanceType{{"x", 15}}}, 363 } 364 for _, tc := range testCases { 365 t.Run(tc.desc, func(t *testing.T) { 366 l := newLedger() 367 l.UpdateInstanceTypes(tc.types) 368 for _, it := range tc.types { 369 if gotV, ok := l.types[it.Type]; !ok || gotV != it { 370 t.Errorf("ledger.types[%s] = %v; want %v", it.Type, gotV, it) 371 } 372 } 373 if len(l.types) != len(tc.types) { 374 t.Errorf("len(ledger.types) = %d; want %d", len(l.types), len(tc.types)) 375 } 376 }) 377 } 378 } 379 380 func TestLedgerResources(t *testing.T) { 381 testCases := []struct { 382 desc string 383 entries map[string]*entry 384 cpuCount int64 385 cpuLimit int64 386 wantInstCount int64 387 }{ 388 {"no-instances", map[string]*entry{}, 2, 3, 0}, 389 {"single-instance", map[string]*entry{"x": {}}, 2, 3, 1}, 390 } 391 for _, tc := range testCases { 392 t.Run(tc.desc, func(t *testing.T) { 393 l := newLedger() 394 l.entries = tc.entries 395 l.cpuQueue.UpdateQuotas(int(tc.cpuCount), int(tc.cpuLimit)) 396 gotR := l.Resources() 397 if gotR.InstCount != tc.wantInstCount { 398 t.Errorf("ledger.instCount = %d; want %d", gotR.InstCount, tc.wantInstCount) 399 } 400 if gotR.CPUUsed != tc.cpuCount { 401 t.Errorf("ledger.cpuCount = %d; want %d", gotR.CPUUsed, tc.cpuCount) 402 } 403 if gotR.CPULimit != tc.cpuLimit { 404 t.Errorf("ledger.cpuLimit = %d; want %d", gotR.CPULimit, tc.cpuLimit) 405 } 406 }) 407 } 408 } 409 410 func TestLedgerResourceTime(t *testing.T) { 411 ct := time.Now() 412 413 testCases := []struct { 414 desc string 415 entries map[string]*entry 416 }{ 417 {"no-instances", map[string]*entry{}}, 418 {"single-instance", map[string]*entry{ 419 "inst-x": { 420 createdAt: ct, 421 instanceID: "id-x", 422 instanceName: "inst-x", 423 vCPUCount: 1, 424 }, 425 }}, 426 {"multiple-instances", map[string]*entry{ 427 "inst-z": { 428 createdAt: ct.Add(2 * time.Second), 429 instanceID: "id-z", 430 instanceName: "inst-z", 431 vCPUCount: 1, 432 }, 433 "inst-y": { 434 createdAt: ct.Add(time.Second), 435 instanceID: "id-y", 436 instanceName: "inst-y", 437 vCPUCount: 1, 438 }, 439 "inst-x": { 440 createdAt: ct, 441 instanceID: "id-x", 442 instanceName: "inst-x", 443 vCPUCount: 1, 444 }, 445 }}, 446 } 447 for _, tc := range testCases { 448 t.Run(tc.desc, func(t *testing.T) { 449 l := newLedger() 450 l.entries = tc.entries 451 gotRT := l.ResourceTime() 452 if !sort.SliceIsSorted(gotRT, func(i, j int) bool { return gotRT[i].Creation.Before(gotRT[j].Creation) }) { 453 t.Errorf("resource time is not sorted") 454 } 455 if len(l.entries) != len(gotRT) { 456 t.Errorf("mismatch in items returned") 457 } 458 for _, rt := range gotRT { 459 delete(l.entries, rt.Name) 460 } 461 if len(l.entries) != 0 { 462 t.Errorf("mismatch") 463 } 464 }) 465 } 466 }