go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/invquery_test.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package engine 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "go.chromium.org/luci/common/clock" 25 "go.chromium.org/luci/common/clock/testclock" 26 "go.chromium.org/luci/gae/filter/featureBreaker" 27 "go.chromium.org/luci/gae/impl/memory" 28 "go.chromium.org/luci/gae/service/datastore" 29 30 "go.chromium.org/luci/scheduler/appengine/internal" 31 "go.chromium.org/luci/scheduler/appengine/task" 32 33 . "github.com/smartystreets/goconvey/convey" 34 ) 35 36 func makeInvListQ(ids ...int64) *invListQuery { 37 invs := make([]*Invocation, len(ids)) 38 for i, id := range ids { 39 invs[i] = &Invocation{ID: id} 40 } 41 return &invListQuery{invs, 0} 42 } 43 44 func invIDs(invs []*Invocation) []int64 { 45 out := make([]int64, len(invs)) 46 for i, inv := range invs { 47 out[i] = inv.ID 48 } 49 return out 50 } 51 52 func TestMergeInvQueries(t *testing.T) { 53 t.Parallel() 54 55 Convey("Empty", t, func() { 56 invs, done, err := mergeInvQueries([]invQuery{ 57 makeInvListQ(), makeInvListQ(), 58 }, 100, nil) 59 So(invs, ShouldBeEmpty) 60 So(done, ShouldBeTrue) 61 So(err, ShouldBeNil) 62 }) 63 64 Convey("Single source, with limit", t, func() { 65 invs, done, err := mergeInvQueries([]invQuery{ 66 makeInvListQ(1, 2, 3, 4, 5), 67 }, 3, nil) 68 So(invIDs(invs), ShouldResemble, []int64{1, 2, 3}) 69 So(done, ShouldBeFalse) 70 So(err, ShouldBeNil) 71 }) 72 73 Convey("Single source, with limit, appends", t, func() { 74 invs := []*Invocation{{ID: 1}, {ID: 2}} 75 invs, done, err := mergeInvQueries([]invQuery{ 76 makeInvListQ(3, 4, 5, 6), 77 }, 3, invs) 78 So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5}) 79 So(done, ShouldBeFalse) 80 So(err, ShouldBeNil) 81 }) 82 83 Convey("Single source, dups and out of order", t, func() { 84 invs, done, err := mergeInvQueries([]invQuery{ 85 makeInvListQ(1, 2, 2, 3, 2, 4, 5), 86 }, 100, nil) 87 So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5}) 88 So(done, ShouldBeTrue) 89 So(err, ShouldBeNil) 90 }) 91 92 Convey("Merging", t, func() { 93 invs, done, err := mergeInvQueries([]invQuery{ 94 makeInvListQ(1, 3, 5), 95 makeInvListQ(2, 4, 6), 96 }, 100, nil) 97 So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5, 6}) 98 So(done, ShouldBeTrue) 99 So(err, ShouldBeNil) 100 }) 101 102 Convey("Merging with dups and limit", t, func() { 103 invs, done, err := mergeInvQueries([]invQuery{ 104 makeInvListQ(1, 2, 3, 4, 5), 105 makeInvListQ(1, 2, 3, 4, 5), 106 }, 3, nil) 107 So(invIDs(invs), ShouldResemble, []int64{1, 2, 3}) 108 So(done, ShouldBeFalse) 109 So(err, ShouldBeNil) 110 }) 111 112 Convey("Merging with limit that exactly matches query size", t, func() { 113 invs, done, err := mergeInvQueries([]invQuery{ 114 makeInvListQ(1, 2, 3, 4, 5), 115 makeInvListQ(1, 2, 3, 4, 5), 116 }, 5, nil) 117 So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5}) 118 So(done, ShouldBeTrue) // true here! this is important, otherwise we'll get empty pages 119 So(err, ShouldBeNil) 120 }) 121 } 122 123 func TestActiveInvQuery(t *testing.T) { 124 t.Parallel() 125 126 Convey("Works", t, func() { 127 q := activeInvQuery(context.Background(), &Job{ 128 ActiveInvocations: []int64{1, 2, 3, 4, 5, 8, 6}, 129 }, 3) 130 So(invIDs(q.invs), ShouldResemble, []int64{4, 5, 6, 8}) 131 }) 132 } 133 134 func TestRecentInvQuery(t *testing.T) { 135 t.Parallel() 136 137 c, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC) 138 now := timestamppb.New(clock.Now(c)) 139 140 Convey("Works", t, func() { 141 q := recentInvQuery(c, &Job{ 142 FinishedInvocationsRaw: marshalFinishedInvs([]*internal.FinishedInvocation{ 143 {InvocationId: 1, Finished: now}, 144 {InvocationId: 2, Finished: now}, 145 {InvocationId: 3, Finished: now}, 146 {InvocationId: 4, Finished: now}, 147 {InvocationId: 5, Finished: now}, 148 {InvocationId: 8, Finished: now}, 149 {InvocationId: 6, Finished: now}, 150 // And this one should be ignored, as it is "too old". 151 {InvocationId: 9, Finished: timestamppb.New(clock.Now(c).Add(-FinishedInvocationsHorizon - 1))}, 152 }), 153 }, 3) 154 So(invIDs(q.invs), ShouldResemble, []int64{4, 5, 6, 8}) 155 }) 156 } 157 158 func TestInvDatastoreIter(t *testing.T) { 159 t.Parallel() 160 161 run := func(c context.Context, query *datastore.Query, limit int) ([]*Invocation, error) { 162 it := invDatastoreIter{} 163 it.start(c, query) 164 defer it.stop() 165 invs := []*Invocation{} 166 for len(invs) != limit { 167 switch inv, err := it.next(); { 168 case err != nil: 169 return nil, err 170 case inv == nil: 171 return invs, nil // fetched everything we had 172 default: 173 invs = append(invs, inv) 174 } 175 } 176 return invs, nil 177 } 178 179 c := memory.Use(context.Background()) 180 181 Convey("Empty", t, func() { 182 invs, err := run(c, datastore.NewQuery("Invocation"), 100) 183 So(err, ShouldBeNil) 184 So(len(invs), ShouldEqual, 0) 185 }) 186 187 Convey("Not empty", t, func() { 188 original := []*Invocation{ 189 {ID: 1}, 190 {ID: 2}, 191 {ID: 3}, 192 {ID: 4}, 193 {ID: 5}, 194 } 195 datastore.Put(c, original) 196 datastore.GetTestable(c).CatchupIndexes() 197 198 Convey("No limit", func() { 199 q := datastore.NewQuery("Invocation").Order("__key__") 200 invs, err := run(c, q, 100) 201 So(err, ShouldBeNil) 202 So(invs, ShouldResemble, original) 203 }) 204 205 Convey("With limit", func() { 206 q := datastore.NewQuery("Invocation").Order("__key__") 207 208 gtq := q 209 invs, err := run(c, gtq, 2) 210 So(err, ShouldBeNil) 211 So(invs, ShouldResemble, original[:2]) 212 213 gtq = q.Gt("__key__", datastore.KeyForObj(c, invs[1])) 214 invs, err = run(c, gtq, 2) 215 So(err, ShouldBeNil) 216 So(invs, ShouldResemble, original[2:4]) 217 218 gtq = q.Gt("__key__", datastore.KeyForObj(c, invs[1])) 219 invs, err = run(c, gtq, 2) 220 So(err, ShouldBeNil) 221 So(invs, ShouldResemble, original[4:5]) 222 223 gtq = q.Gt("__key__", datastore.KeyForObj(c, invs[0])) 224 invs, err = run(c, gtq, 2) 225 So(err, ShouldBeNil) 226 So(invs, ShouldBeEmpty) 227 }) 228 229 Convey("With error", func() { 230 dsErr := fmt.Errorf("boo") 231 232 brokenC, breaker := featureBreaker.FilterRDS(c, nil) 233 breaker.BreakFeatures(dsErr, "Run") 234 235 q := datastore.NewQuery("Invocation").Order("__key__") 236 invs, err := run(brokenC, q, 100) 237 So(err, ShouldEqual, dsErr) 238 So(len(invs), ShouldEqual, 0) 239 }) 240 }) 241 } 242 243 func insertInv(c context.Context, jobID string, invID int64, status task.Status) *Invocation { 244 inv := &Invocation{ 245 ID: invID, 246 JobID: jobID, 247 Status: status, 248 } 249 if status.Final() { 250 inv.IndexedJobID = jobID 251 } 252 if err := datastore.Put(c, inv); err != nil { 253 panic(err) 254 } 255 datastore.GetTestable(c).CatchupIndexes() 256 return inv 257 } 258 259 func TestFinishedInvQuery(t *testing.T) { 260 t.Parallel() 261 262 fetchAll := func(q *invDatastoreQuery) (out []*Invocation) { 263 defer q.close() 264 for { 265 inv, err := q.peek() 266 if err != nil { 267 panic(err) 268 } 269 if inv == nil { 270 return 271 } 272 out = append(out, inv) 273 if err := q.advance(); err != nil { 274 panic(err) 275 } 276 } 277 } 278 279 Convey("With context", t, func() { 280 c := memory.Use(context.Background()) 281 282 Convey("Empty", func() { 283 q := finishedInvQuery(c, &Job{JobID: "proj/job"}, 0) 284 So(fetchAll(q), ShouldBeEmpty) 285 }) 286 287 Convey("Non empty", func() { 288 insertInv(c, "proj/job", 3, task.StatusSucceeded) 289 insertInv(c, "proj/job", 2, task.StatusSucceeded) 290 insertInv(c, "proj/job", 1, task.StatusSucceeded) 291 292 Convey("no cursor", func() { 293 q := finishedInvQuery(c, &Job{JobID: "proj/job"}, 0) 294 So(invIDs(fetchAll(q)), ShouldResemble, []int64{1, 2, 3}) 295 }) 296 297 Convey("with cursor", func() { 298 q := finishedInvQuery(c, &Job{JobID: "proj/job"}, 1) 299 So(invIDs(fetchAll(q)), ShouldResemble, []int64{2, 3}) 300 }) 301 }) 302 }) 303 } 304 305 func TestFetchInvsPage(t *testing.T) { 306 t.Parallel() 307 308 Convey("With context", t, func() { 309 const jobID = "proj/job" 310 311 c := memory.Use(context.Background()) 312 313 // Note: we use only two queries here for simplicity, since various cases 314 // involving 3 queries are not significantly different (and differences are 315 // tested separately by other tests). 316 makeQS := func(job *Job, opts ListInvocationsOpts, lastScanned int64) []invQuery { 317 qs := []invQuery{} 318 if !opts.ActiveOnly { 319 ds := finishedInvQuery(c, job, lastScanned) 320 Reset(ds.close) 321 qs = append(qs, ds) 322 } 323 if !opts.FinishedOnly { 324 qs = append(qs, activeInvQuery(c, job, lastScanned)) 325 } 326 return qs 327 } 328 329 fetchAllPages := func(qs []invQuery, opts ListInvocationsOpts) (invs []*Invocation, pages []invsPage) { 330 var page invsPage 331 var err error 332 for { 333 before := len(invs) 334 invs, page, err = fetchInvsPage(c, qs, opts, invs) 335 So(err, ShouldBeNil) 336 So(len(invs)-before, ShouldEqual, page.count) 337 So(page.count, ShouldBeLessThanOrEqualTo, opts.PageSize) 338 pages = append(pages, page) 339 if page.final { 340 return 341 } 342 } 343 } 344 345 Convey("ActiveInvocations list is consistent with datastore", func() { 346 // List of finished invocations, oldest to newest. 347 i6 := insertInv(c, jobID, 6, task.StatusSucceeded) 348 i5 := insertInv(c, jobID, 5, task.StatusFailed) 349 i4 := insertInv(c, jobID, 4, task.StatusSucceeded) 350 // List of still running invocations, oldest to newest. 351 i3 := insertInv(c, jobID, 3, task.StatusRunning) 352 i2 := insertInv(c, jobID, 2, task.StatusRunning) 353 i1 := insertInv(c, jobID, 1, task.StatusRunning) 354 355 job := &Job{ 356 JobID: jobID, 357 ActiveInvocations: []int64{ 358 3, 1, 2, // the set of active invocations, unordered 359 }, 360 } 361 362 Convey("No paging", func() { 363 opts := ListInvocationsOpts{PageSize: 7} 364 invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil) 365 So(err, ShouldBeNil) 366 So(page, ShouldResemble, invsPage{6, true, 6}) 367 So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6}) 368 }) 369 370 Convey("No paging, active only", func() { 371 opts := ListInvocationsOpts{PageSize: 7, ActiveOnly: true} 372 invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil) 373 So(err, ShouldBeNil) 374 So(page, ShouldResemble, invsPage{3, true, 3}) 375 So(invs, ShouldResemble, []*Invocation{i1, i2, i3}) 376 }) 377 378 Convey("No paging, finished only", func() { 379 opts := ListInvocationsOpts{PageSize: 7, FinishedOnly: true} 380 invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil) 381 So(err, ShouldBeNil) 382 So(page, ShouldResemble, invsPage{3, true, 6}) 383 So(invs, ShouldResemble, []*Invocation{i4, i5, i6}) 384 }) 385 386 Convey("Paging", func() { 387 opts := ListInvocationsOpts{PageSize: 2} 388 invs, pages := fetchAllPages(makeQS(job, opts, 0), opts) 389 So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6}) 390 So(pages, ShouldResemble, []invsPage{ 391 {2, false, 2}, 392 {2, false, 4}, 393 {2, true, 6}, 394 }) 395 }) 396 397 Convey("Paging, resuming from cursor", func() { 398 opts := ListInvocationsOpts{PageSize: 2} 399 invs, pages := fetchAllPages(makeQS(job, opts, 3), opts) 400 So(invs, ShouldResemble, []*Invocation{i4, i5, i6}) 401 So(pages, ShouldResemble, []invsPage{ 402 {2, false, 5}, 403 {1, true, 6}, 404 }) 405 }) 406 407 Convey("Paging, active only", func() { 408 opts := ListInvocationsOpts{PageSize: 2, ActiveOnly: true} 409 invs, pages := fetchAllPages(makeQS(job, opts, 0), opts) 410 So(invs, ShouldResemble, []*Invocation{i1, i2, i3}) 411 So(pages, ShouldResemble, []invsPage{ 412 {2, false, 2}, 413 {1, true, 3}, 414 }) 415 }) 416 417 Convey("Paging, finished only", func() { 418 opts := ListInvocationsOpts{PageSize: 2, FinishedOnly: true} 419 invs, pages := fetchAllPages(makeQS(job, opts, 0), opts) 420 So(invs, ShouldResemble, []*Invocation{i4, i5, i6}) 421 So(pages, ShouldResemble, []invsPage{ 422 {2, false, 5}, 423 {1, true, 6}, 424 }) 425 }) 426 }) 427 428 Convey("ActiveInvocations list is stale", func() { 429 // List of finished invocations, oldest to newest. 430 i6 := insertInv(c, jobID, 6, task.StatusSucceeded) 431 i5 := insertInv(c, jobID, 5, task.StatusFailed) 432 i4 := insertInv(c, jobID, 4, task.StatusSucceeded) 433 // List of still invocations referenced by ActiveInvocations. 434 i3 := insertInv(c, jobID, 3, task.StatusSucceeded) // actually done! 435 i2 := insertInv(c, jobID, 2, task.StatusRunning) 436 i1 := insertInv(c, jobID, 1, task.StatusRunning) 437 438 job := &Job{ 439 JobID: jobID, 440 ActiveInvocations: []int64{3, 1, 2}, 441 } 442 443 Convey("No paging", func() { 444 opts := ListInvocationsOpts{PageSize: 7} 445 invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil) 446 So(err, ShouldBeNil) 447 So(page, ShouldResemble, invsPage{6, true, 6}) 448 So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6}) 449 }) 450 451 Convey("No paging, active only", func() { 452 opts := ListInvocationsOpts{PageSize: 7, ActiveOnly: true} 453 invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil) 454 So(err, ShouldBeNil) 455 So(page, ShouldResemble, invsPage{2, true, 3}) // 3 was scanned and skipped! 456 So(invs, ShouldResemble, []*Invocation{i1, i2}) 457 }) 458 459 Convey("No paging, finished only", func() { 460 opts := ListInvocationsOpts{PageSize: 7, FinishedOnly: true} 461 invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil) 462 So(err, ShouldBeNil) 463 So(page, ShouldResemble, invsPage{4, true, 6}) 464 So(invs, ShouldResemble, []*Invocation{i3, i4, i5, i6}) 465 }) 466 467 Convey("Paging", func() { 468 opts := ListInvocationsOpts{PageSize: 2} 469 invs, pages := fetchAllPages(makeQS(job, opts, 0), opts) 470 So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6}) 471 So(pages, ShouldResemble, []invsPage{ 472 {2, false, 2}, 473 {2, false, 4}, 474 {2, true, 6}, 475 }) 476 }) 477 478 Convey("Paging, resuming from cursor", func() { 479 opts := ListInvocationsOpts{PageSize: 2} 480 invs, pages := fetchAllPages(makeQS(job, opts, 3), opts) 481 So(invs, ShouldResemble, []*Invocation{i4, i5, i6}) 482 So(pages, ShouldResemble, []invsPage{ 483 {2, false, 5}, 484 {1, true, 6}, 485 }) 486 }) 487 488 Convey("Paging, active only", func() { 489 opts := ListInvocationsOpts{PageSize: 1, ActiveOnly: true} 490 invs, pages := fetchAllPages(makeQS(job, opts, 0), opts) 491 So(invs, ShouldResemble, []*Invocation{i1, i2}) 492 So(pages, ShouldResemble, []invsPage{ 493 {1, false, 1}, 494 {1, false, 2}, 495 {0, true, 3}, // empty mini-page, but advanced cursor 496 }) 497 }) 498 499 Convey("Paging, finished only", func() { 500 opts := ListInvocationsOpts{PageSize: 2, FinishedOnly: true} 501 invs, pages := fetchAllPages(makeQS(job, opts, 0), opts) 502 So(invs, ShouldResemble, []*Invocation{i3, i4, i5, i6}) 503 So(pages, ShouldResemble, []invsPage{ 504 {2, false, 4}, 505 {2, true, 6}, 506 }) 507 }) 508 }) 509 }) 510 } 511 512 func TestFillShallowInvs(t *testing.T) { 513 t.Parallel() 514 515 Convey("With context", t, func() { 516 c := memory.Use(context.Background()) 517 518 // Bodies for inflated items. 519 datastore.Put(c, []*Invocation{ 520 {ID: 1, Status: task.StatusSucceeded}, 521 {ID: 10, Status: task.StatusRunning}, 522 }) 523 524 shallow := []*Invocation{ 525 {ID: 1}, // to be inflated 526 {ID: 2, Status: task.StatusSucceeded}, 527 {ID: 3, Status: task.StatusRunning}, 528 {ID: 10}, // to be inflated 529 {ID: 10}, // to be inflated, again... as an edge case 530 {ID: 11, Status: task.StatusSucceeded}, 531 } 532 533 Convey("no filtering", func() { 534 fat, err := fillShallowInvs(c, shallow, ListInvocationsOpts{}) 535 So(err, ShouldBeNil) 536 So(fat, ShouldResemble, []*Invocation{ 537 {ID: 1, Status: task.StatusSucceeded}, 538 {ID: 2, Status: task.StatusSucceeded}, 539 {ID: 3, Status: task.StatusRunning}, 540 {ID: 10, Status: task.StatusRunning}, 541 {ID: 10, Status: task.StatusRunning}, 542 {ID: 11, Status: task.StatusSucceeded}, 543 }) 544 So(&shallow[0], ShouldEqual, &fat[0]) // same backing array 545 }) 546 547 Convey("finished only", func() { 548 fat, err := fillShallowInvs(c, shallow, ListInvocationsOpts{ 549 FinishedOnly: true, 550 }) 551 So(err, ShouldBeNil) 552 So(fat, ShouldResemble, []*Invocation{ 553 {ID: 1, Status: task.StatusSucceeded}, 554 {ID: 2, Status: task.StatusSucceeded}, 555 {ID: 11, Status: task.StatusSucceeded}, 556 }) 557 So(&shallow[0], ShouldEqual, &fat[0]) // same backing array 558 }) 559 560 Convey("active only", func() { 561 fat, err := fillShallowInvs(c, shallow, ListInvocationsOpts{ 562 ActiveOnly: true, 563 }) 564 So(err, ShouldBeNil) 565 So(fat, ShouldResemble, []*Invocation{ 566 {ID: 3, Status: task.StatusRunning}, 567 {ID: 10, Status: task.StatusRunning}, 568 {ID: 10, Status: task.StatusRunning}, 569 }) 570 So(&shallow[0], ShouldEqual, &fat[0]) // same backing array 571 }) 572 }) 573 }