github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/lifecycle.go (about) 1 package model 2 3 import ( 4 "fmt" 5 "sort" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/evergreen-ci/evergreen" 11 "github.com/evergreen-ci/evergreen/db" 12 "github.com/evergreen-ci/evergreen/model/build" 13 "github.com/evergreen-ci/evergreen/model/patch" 14 "github.com/evergreen-ci/evergreen/model/task" 15 "github.com/evergreen-ci/evergreen/model/version" 16 "github.com/evergreen-ci/evergreen/util" 17 "github.com/mongodb/grip" 18 "github.com/pkg/errors" 19 "gopkg.in/mgo.v2" 20 "gopkg.in/mgo.v2/bson" 21 ) 22 23 const ( 24 AllDependencies = "*" 25 AllVariants = "*" 26 AllStatuses = "*" 27 ) 28 29 // cacheFromTask is helper for creating a build.TaskCache from a real Task model. 30 func cacheFromTask(t task.Task) build.TaskCache { 31 return build.TaskCache{ 32 Id: t.Id, 33 DisplayName: t.DisplayName, 34 Status: t.Status, 35 StatusDetails: t.Details, 36 StartTime: t.StartTime, 37 TimeTaken: t.TimeTaken, 38 Activated: t.Activated, 39 } 40 } 41 42 // SetVersionActivation updates the "active" state of all builds and tasks associated with a 43 // version to the given setting. It also updates the task cache for all builds affected. 44 func SetVersionActivation(versionId string, active bool, caller string) error { 45 builds, err := build.Find( 46 build.ByVersion(versionId).WithFields(build.IdKey), 47 ) 48 if err != nil { 49 return err 50 } 51 for _, b := range builds { 52 err = SetBuildActivation(b.Id, active, caller) 53 if err != nil { 54 return err 55 } 56 } 57 return nil 58 } 59 60 // SetBuildActivation updates the "active" state of this build and all associated tasks. 61 // It also updates the task cache for the build document. 62 func SetBuildActivation(buildId string, active bool, caller string) error { 63 var err error 64 65 // If activating a task, set the ActivatedBy field to be the caller 66 if active { 67 _, err = task.UpdateAll( 68 bson.M{ 69 task.BuildIdKey: buildId, 70 task.StatusKey: evergreen.TaskUndispatched, 71 }, 72 bson.M{"$set": bson.M{task.ActivatedKey: active, task.ActivatedByKey: caller}}, 73 ) 74 } else { 75 76 // if trying to deactivate a task then only deactivate tasks that have not been activated by a user. 77 // if the caller is the default task activator, 78 // only deactivate tasks that are activated by the default task activator 79 if evergreen.IsSystemActivator(caller) { 80 _, err = task.UpdateAll( 81 bson.M{ 82 task.BuildIdKey: buildId, 83 task.StatusKey: evergreen.TaskUndispatched, 84 task.ActivatedByKey: caller, 85 }, 86 bson.M{"$set": bson.M{task.ActivatedKey: active, task.ActivatedByKey: caller}}, 87 ) 88 89 } else { 90 // update all tasks if the caller is not evergreen. 91 _, err = task.UpdateAll( 92 bson.M{ 93 task.BuildIdKey: buildId, 94 task.StatusKey: evergreen.TaskUndispatched, 95 }, 96 bson.M{"$set": bson.M{task.ActivatedKey: active, task.ActivatedByKey: caller}}, 97 ) 98 } 99 } 100 101 if err != nil { 102 return err 103 } 104 if err = build.UpdateActivation(buildId, active, caller); err != nil { 105 return err 106 } 107 return RefreshTasksCache(buildId) 108 } 109 110 // AbortBuild sets the abort flag on all tasks associated with the build which are in an abortable 111 // state, and marks the build as deactivated. 112 func AbortBuild(buildId string, caller string) error { 113 err := task.AbortBuild(buildId) 114 if err != nil { 115 return err 116 } 117 return build.UpdateActivation(buildId, false, caller) 118 } 119 120 // AbortVersion sets the abort flag on all tasks associated with the version which are in an 121 // abortable state 122 func AbortVersion(versionId string) error { 123 _, err := task.UpdateAll( 124 bson.M{ 125 task.VersionKey: versionId, 126 task.StatusKey: bson.M{"$in": evergreen.AbortableStatuses}, 127 }, 128 bson.M{"$set": bson.M{task.AbortedKey: true}}, 129 ) 130 return err 131 } 132 133 func MarkVersionStarted(versionId string, startTime time.Time) error { 134 return version.UpdateOne( 135 bson.M{version.IdKey: versionId}, 136 bson.M{"$set": bson.M{ 137 version.StartTimeKey: startTime, 138 version.StatusKey: evergreen.VersionStarted, 139 }}, 140 ) 141 } 142 143 // MarkVersionCompleted updates the status of a completed version to reflect its correct state by 144 // checking the status of its individual builds. 145 func MarkVersionCompleted(versionId string, finishTime time.Time) error { 146 status := evergreen.VersionSucceeded 147 148 // Find the statuses for all builds in the version so we can figure out the version's status 149 builds, err := build.Find( 150 build.ByVersion(versionId).WithFields(build.StatusKey), 151 ) 152 if err != nil { 153 return err 154 } 155 156 for _, b := range builds { 157 if !b.IsFinished() { 158 return nil 159 } 160 if b.Status != evergreen.BuildSucceeded { 161 status = evergreen.VersionFailed 162 } 163 } 164 return version.UpdateOne( 165 bson.M{version.IdKey: versionId}, 166 bson.M{"$set": bson.M{ 167 version.FinishTimeKey: finishTime, 168 version.StatusKey: status, 169 }}, 170 ) 171 } 172 173 // SetBuildPriority updates the priority field of all tasks associated with the given build id. 174 func SetBuildPriority(buildId string, priority int64) error { 175 modifier := bson.M{task.PriorityKey: priority} 176 //blacklisted - these tasks should never run, so unschedule now 177 if priority < 0 { 178 modifier[task.ActivatedKey] = false 179 } 180 181 _, err := task.UpdateAll( 182 bson.M{task.BuildIdKey: buildId}, 183 bson.M{"$set": modifier}, 184 ) 185 return err 186 } 187 188 // SetVersionPriority updates the priority field of all tasks associated with the given build id. 189 190 func SetVersionPriority(versionId string, priority int64) error { 191 modifier := bson.M{task.PriorityKey: priority} 192 //blacklisted - these tasks should never run, so unschedule now 193 if priority < 0 { 194 modifier[task.ActivatedKey] = false 195 } 196 197 _, err := task.UpdateAll( 198 bson.M{task.VersionKey: versionId}, 199 bson.M{"$set": modifier}, 200 ) 201 return err 202 } 203 204 // RestartVersion restarts completed tasks associated with a given versionId. 205 // If abortInProgress is true, it also sets the abort flag on any in-progress tasks. 206 func RestartVersion(versionId string, taskIds []string, abortInProgress bool, caller string) error { 207 // restart all the 'not in-progress' tasks for the version 208 allTasks, err := task.Find(task.ByDispatchedWithIdsVersionAndStatus(taskIds, versionId, task.CompletedStatuses)) 209 210 if err != nil && err != mgo.ErrNotFound { 211 return err 212 } 213 214 // archive all the tasks 215 for _, t := range allTasks { 216 if err = t.Archive(); err != nil { 217 return errors.Wrap(err, "failed to archive task") 218 } 219 } 220 221 // Set all the task fields to indicate restarted 222 if err = task.ResetTasks(taskIds); err != nil { 223 return errors.WithStack(err) 224 } 225 226 // TODO figure out a way to coalesce updates for task cache for the same build, so we 227 // only need to do one update per-build instead of one per-task here. 228 // Doesn't seem to be possible as-is because $ can only apply to one array element matched per 229 // document. 230 buildIdSet := map[string]bool{} 231 for _, t := range allTasks { 232 buildIdSet[t.BuildId] = true 233 if err = build.ResetCachedTask(t.BuildId, t.Id); err != nil { 234 return errors.WithStack(err) 235 } 236 } 237 238 // reset the build statuses, once per build 239 buildIdList := make([]string, 0, len(buildIdSet)) 240 for k := range buildIdSet { 241 buildIdList = append(buildIdList, k) 242 } 243 244 // Set the build status for all the builds containing the tasks that we touched 245 _, err = build.UpdateAllBuilds( 246 bson.M{build.IdKey: bson.M{"$in": buildIdList}}, 247 bson.M{"$set": bson.M{build.StatusKey: evergreen.BuildStarted}}, 248 ) 249 250 if err != nil { 251 return errors.WithStack(err) 252 } 253 254 if abortInProgress { 255 // abort in-progress tasks in this build 256 _, err = task.UpdateAll( 257 bson.M{ 258 task.VersionKey: versionId, 259 task.IdKey: bson.M{"$in": taskIds}, 260 task.StatusKey: bson.M{"$in": evergreen.AbortableStatuses}, 261 }, 262 bson.M{"$set": bson.M{task.AbortedKey: true}}, 263 ) 264 265 if err != nil { 266 return errors.WithStack(err) 267 } 268 } 269 270 // update activation for all the builds 271 for _, b := range buildIdList { 272 if err := build.UpdateActivation(b, true, caller); err != nil { 273 return errors.WithStack(err) 274 } 275 } 276 return nil 277 278 } 279 280 // RestartBuild restarts completed tasks associated with a given buildId. 281 // If abortInProgress is true, it also sets the abort flag on any in-progress tasks. 282 func RestartBuild(buildId string, taskIds []string, abortInProgress bool, caller string) error { 283 // restart all the 'not in-progress' tasks for the build 284 allTasks, err := task.Find(task.ByIdsBuildAndStatus(taskIds, buildId, task.CompletedStatuses)) 285 if err != nil && err != mgo.ErrNotFound { 286 return errors.WithStack(err) 287 } 288 289 for _, t := range allTasks { 290 if t.DispatchTime != util.ZeroTime { 291 err = resetTask(t.Id) 292 if err != nil { 293 return errors.Wrapf(err, 294 "Restarting build %v failed, could not task.reset on task", 295 buildId, t.Id) 296 } 297 } 298 } 299 300 if abortInProgress { 301 // abort in-progress tasks in this build 302 _, err = task.UpdateAll( 303 bson.M{ 304 task.BuildIdKey: buildId, 305 task.StatusKey: bson.M{ 306 "$in": evergreen.AbortableStatuses, 307 }, 308 }, 309 bson.M{ 310 "$set": bson.M{ 311 task.AbortedKey: true, 312 }, 313 }, 314 ) 315 if err != nil { 316 return errors.WithStack(err) 317 } 318 } 319 320 return errors.WithStack(build.UpdateActivation(buildId, true, caller)) 321 } 322 323 func CreateTasksCache(tasks []task.Task) []build.TaskCache { 324 tasks = sortTasks(tasks) 325 cache := make([]build.TaskCache, 0, len(tasks)) 326 for _, task := range tasks { 327 cache = append(cache, cacheFromTask(task)) 328 } 329 return cache 330 } 331 332 // RefreshTasksCache updates a build document so that the tasks cache reflects the correct current 333 // state of the tasks it represents. 334 func RefreshTasksCache(buildId string) error { 335 tasks, err := task.Find(task.ByBuildId(buildId).WithFields(task.IdKey, task.DisplayNameKey, task.StatusKey, 336 task.DetailsKey, task.StartTimeKey, task.TimeTakenKey, task.ActivatedKey, task.DependsOnKey)) 337 if err != nil { 338 return errors.WithStack(err) 339 } 340 cache := CreateTasksCache(tasks) 341 return errors.WithStack(build.SetTasksCache(buildId, cache)) 342 } 343 344 //AddTasksToBuild creates the tasks for the given build of a project 345 func AddTasksToBuild(b *build.Build, project *Project, v *version.Version, 346 taskNames []string) (*build.Build, error) { 347 348 // find the build variant for this project/build 349 buildVariant := project.FindBuildVariant(b.BuildVariant) 350 if buildVariant == nil { 351 return nil, errors.Errorf("Could not find build %v in %v project file", 352 b.BuildVariant, project.Identifier) 353 } 354 355 // create the new tasks for the build 356 tasks, err := createTasksForBuild( 357 project, buildVariant, b, v, NewTaskIdTable(project, v), taskNames) 358 if err != nil { 359 return nil, errors.Wrapf(err, "error creating tasks for build %s", b.Id) 360 } 361 362 // insert the tasks into the db 363 for _, task := range tasks { 364 grip.Infoln("Creating task:", task.DisplayName) 365 if err := task.Insert(); err != nil { 366 return nil, errors.Wrapf(err, "error inserting task %s", task.Id) 367 } 368 } 369 370 // update the build to hold the new tasks 371 if err := RefreshTasksCache(b.Id); err != nil { 372 return nil, errors.Wrapf(err, "error updating task cache for %s", b.Id) 373 } 374 375 return b, nil 376 } 377 378 // CreateBuildFromVersion creates a build given all of the necessary information 379 // from the corresponding version and project and a list of tasks. 380 func CreateBuildFromVersion(project *Project, v *version.Version, tt TaskIdTable, 381 buildName string, activated bool, taskNames []string) (string, error) { 382 383 grip.Debugf("Creating %v %v build, activated: %v", v.Requester, buildName, activated) 384 385 // find the build variant for this project/build 386 buildVariant := project.FindBuildVariant(buildName) 387 if buildVariant == nil { 388 return "", errors.Errorf("could not find build %v in %v project file", buildName, project.Identifier) 389 } 390 391 // create a new build id 392 buildId := util.CleanName( 393 fmt.Sprintf("%v_%v_%v_%v", 394 project.Identifier, 395 buildName, 396 v.Revision, 397 v.CreateTime.Format(build.IdTimeLayout))) 398 399 // create the build itself 400 b := &build.Build{ 401 Id: buildId, 402 CreateTime: v.CreateTime, 403 PushTime: v.CreateTime, 404 Activated: activated, 405 Project: project.Identifier, 406 Revision: v.Revision, 407 Status: evergreen.BuildCreated, 408 BuildVariant: buildName, 409 Version: v.Id, 410 DisplayName: buildVariant.DisplayName, 411 RevisionOrderNumber: v.RevisionOrderNumber, 412 Requester: v.Requester, 413 } 414 415 // get a new build number for the build 416 buildNumber, err := db.GetNewBuildVariantBuildNumber(buildName) 417 if err != nil { 418 return "", errors.Wrapf(err, "could not get build number for build variant"+ 419 " %v in %v project file", buildName, project.Identifier) 420 } 421 b.BuildNumber = strconv.FormatUint(buildNumber, 10) 422 423 // create all of the necessary tasks for the build 424 tasksForBuild, err := createTasksForBuild(project, buildVariant, b, v, tt, taskNames) 425 if err != nil { 426 return "", errors.Wrapf(err, "error creating tasks for build %s", b.Id) 427 } 428 429 // insert all of the build's tasks into the db 430 for _, task := range tasksForBuild { 431 if err := task.Insert(); err != nil { 432 return "", errors.Wrapf(err, "error inserting task %s", task.Id) 433 } 434 } 435 436 // create task caches for all of the tasks, and place them into the build 437 tasks := make([]task.Task, 0, len(tasksForBuild)) 438 for _, taskP := range tasksForBuild { 439 tasks = append(tasks, *taskP) 440 } 441 b.Tasks = CreateTasksCache(tasks) 442 443 // insert the build 444 if err := b.Insert(); err != nil { 445 return "", errors.Wrapf(err, "error inserting build %v", b.Id) 446 } 447 448 // success! 449 return b.Id, nil 450 } 451 452 // createTasksForBuild creates all of the necessary tasks for the build. Returns a 453 // slice of all of the tasks created, as well as an error if any occurs. 454 // The slice of tasks will be in the same order as the project's specified tasks 455 // appear in the specified build variant. 456 func createTasksForBuild(project *Project, buildVariant *BuildVariant, 457 b *build.Build, v *version.Version, tt TaskIdTable, taskNames []string) ([]*task.Task, error) { 458 459 // the list of tasks we should create. if tasks are passed in, then 460 // use those, else use the default set 461 tasksToCreate := []BuildVariantTask{} 462 createAll := len(taskNames) == 0 463 for _, task := range buildVariant.Tasks { 464 // get the task spec out of the project 465 taskSpec := project.GetSpecForTask(task.Name) 466 467 // sanity check that the config isn't malformed 468 if taskSpec.Name == "" { 469 return nil, errors.Errorf("config is malformed: variant '%v' runs "+ 470 "task called '%v' but no such task exists for repo %v for "+ 471 "version %v", buildVariant.Name, task.Name, project.Identifier, v.Id) 472 } 473 474 // update task document with spec fields 475 task.Populate(taskSpec) 476 477 if ((task.Patchable != nil && !*task.Patchable) || task.Name == evergreen.PushStage) && //TODO remove PushStage 478 b.Requester == evergreen.PatchVersionRequester { 479 continue 480 } 481 if createAll || util.SliceContains(taskNames, task.Name) { 482 tasksToCreate = append(tasksToCreate, task) 483 } 484 } 485 486 // if any tasks already exist in the build, add them to the id table 487 // so they can be used as dependencies 488 for _, task := range b.Tasks { 489 tt.AddId(b.BuildVariant, task.DisplayName, task.Id) 490 } 491 492 // create and insert all of the actual tasks 493 tasks := make([]*task.Task, 0, len(tasksToCreate)) 494 for _, t := range tasksToCreate { 495 newTask := createOneTask(tt.GetId(b.BuildVariant, t.Name), t, project, buildVariant, b, v) 496 497 // set Tags based on the spec 498 newTask.Tags = project.GetSpecForTask(t.Name).Tags 499 500 // set the new task's dependencies 501 if len(t.DependsOn) == 1 && 502 t.DependsOn[0].Name == AllDependencies && 503 t.DependsOn[0].Variant != AllVariants { 504 // the task depends on all of the other tasks in the build 505 newTask.DependsOn = make([]task.Dependency, 0, len(tasksToCreate)-1) 506 for _, dep := range tasksToCreate { 507 status := evergreen.TaskSucceeded 508 if t.DependsOn[0].Status != "" { 509 status = t.DependsOn[0].Status 510 } 511 id := tt.GetId(b.BuildVariant, dep.Name) 512 if len(id) == 0 || dep.Name == newTask.DisplayName { 513 continue 514 } 515 newTask.DependsOn = append(newTask.DependsOn, task.Dependency{TaskId: id, Status: status}) 516 } 517 } else { 518 // the task has specific dependencies 519 newTask.DependsOn = make([]task.Dependency, 0, len(t.DependsOn)) 520 for _, dep := range t.DependsOn { 521 // only add as a dependency if the dependency is valid/exists 522 status := evergreen.TaskSucceeded 523 if dep.Status != "" { 524 status = dep.Status 525 } 526 bv := b.BuildVariant 527 if dep.Variant != "" { 528 bv = dep.Variant 529 } 530 531 newDeps := []task.Dependency{} 532 533 if dep.Variant == AllVariants { 534 // for * case, we need to add all variants of the task 535 var ids []string 536 if dep.Name != AllDependencies { 537 ids = tt.GetIdsForAllVariantsExcluding( 538 dep.Name, 539 TVPair{TaskName: newTask.DisplayName, Variant: newTask.BuildVariant}, 540 ) 541 } else { 542 // edge case where variant and task are both * 543 ids = tt.GetIdsForAllTasks(b.BuildVariant, newTask.DisplayName) 544 } 545 for _, id := range ids { 546 if len(id) != 0 { 547 newDeps = append(newDeps, task.Dependency{TaskId: id, Status: status}) 548 } 549 } 550 } else { 551 // general case 552 id := tt.GetId(bv, dep.Name) 553 // only create the dependency if the task exists--it always will, 554 // except for patches with patch_optional dependencies. 555 if len(id) != 0 { 556 newDeps = []task.Dependency{{TaskId: id, Status: status}} 557 } 558 } 559 560 newTask.DependsOn = append(newTask.DependsOn, newDeps...) 561 } 562 } 563 564 // append the task to the list of the created tasks 565 tasks = append(tasks, newTask) 566 } 567 568 // Set the NumDependents field 569 // Existing tasks in the db and tasks in other builds are not updated 570 setNumDeps(tasks) 571 572 // return all of the tasks created 573 return tasks, nil 574 } 575 576 // setNumDeps sets NumDependents for each task in tasks. 577 // NumDependents is the number of tasks depending on the task. Only tasks created at the same time 578 // and in the same variant are included. 579 func setNumDeps(tasks []*task.Task) { 580 idToTask := make(map[string]*task.Task) 581 for i, task := range tasks { 582 idToTask[task.Id] = tasks[i] 583 } 584 585 for _, task := range tasks { 586 // Recursively find all tasks that task depends on and increments their NumDependents field 587 setNumDepsRec(task, idToTask, make(map[string]bool)) 588 } 589 590 return 591 } 592 593 // setNumDepsRec recursively finds all tasks that task depends on and increments their NumDependents field. 594 // tasks not in idToTasks are not affected. 595 func setNumDepsRec(task *task.Task, idToTasks map[string]*task.Task, seen map[string]bool) { 596 for _, dep := range task.DependsOn { 597 // Check whether this dependency is included in the tasks we're currently creating 598 if depTask, ok := idToTasks[dep.TaskId]; ok { 599 if !seen[depTask.Id] { 600 seen[depTask.Id] = true 601 depTask.NumDependents = depTask.NumDependents + 1 602 setNumDepsRec(depTask, idToTasks, seen) 603 } 604 } 605 } 606 } 607 608 // TryMarkPatchBuildFinished attempts to mark a patch as finished if all 609 // the builds for the patch are finished as well 610 func TryMarkPatchBuildFinished(b *build.Build, finishTime time.Time) error { 611 v, err := version.FindOne(version.ById(b.Version)) 612 if err != nil { 613 return errors.WithStack(err) 614 } 615 if v == nil { 616 return errors.Errorf("Cannot find version for build %v with version %v", b.Id, b.Version) 617 } 618 619 // ensure all builds for this patch are finished as well 620 builds, err := build.Find(build.ByIds(v.BuildIds).WithFields(build.StatusKey)) 621 if err != nil { 622 return err 623 } 624 625 patchCompleted := true 626 status := evergreen.PatchSucceeded 627 for _, build := range builds { 628 if !build.IsFinished() { 629 patchCompleted = false 630 } 631 if build.Status != evergreen.BuildSucceeded { 632 status = evergreen.PatchFailed 633 } 634 } 635 636 // nothing to do if the patch isn't completed 637 if !patchCompleted { 638 return nil 639 } 640 641 return errors.WithStack(patch.TryMarkFinished(v.Id, finishTime, status)) 642 } 643 644 // createOneTask is a helper to create a single task. 645 func createOneTask(id string, buildVarTask BuildVariantTask, project *Project, 646 buildVariant *BuildVariant, b *build.Build, v *version.Version) *task.Task { 647 return &task.Task{ 648 Id: id, 649 Secret: util.RandomString(), 650 DisplayName: buildVarTask.Name, 651 BuildId: b.Id, 652 BuildVariant: buildVariant.Name, 653 CreateTime: b.CreateTime, 654 PushTime: b.PushTime, 655 ScheduledTime: util.ZeroTime, 656 StartTime: util.ZeroTime, // Certain time fields must be initialized 657 FinishTime: util.ZeroTime, // to our own util.ZeroTime value (which is 658 DispatchTime: util.ZeroTime, // Unix epoch 0, not Go's time.Time{}) 659 LastHeartbeat: util.ZeroTime, 660 Status: evergreen.TaskUndispatched, 661 Activated: b.Activated, 662 RevisionOrderNumber: v.RevisionOrderNumber, 663 Requester: v.Requester, 664 Version: v.Id, 665 Revision: v.Revision, 666 Project: project.Identifier, 667 Priority: buildVarTask.Priority, 668 } 669 } 670 671 // DeleteBuild removes any record of the build by removing it and all of the tasks that 672 // are a part of it from the database. 673 func DeleteBuild(id string) error { 674 err := task.RemoveAllWithBuild(id) 675 if err != nil && err != mgo.ErrNotFound { 676 return errors.WithStack(err) 677 } 678 return errors.WithStack(build.Remove(id)) 679 } 680 681 // sortTasks topologically sorts the tasks by dependency, grouping tasks with common dependencies, 682 // and alphabetically sorting within groups. 683 // All tasks with cross-variant dependencies are at the far right. 684 func sortTasks(tasks []task.Task) []task.Task { 685 // Separate out tasks with cross-variant dependencies 686 taskPresent := make(map[string]bool) 687 for _, task := range tasks { 688 taskPresent[task.Id] = true 689 } 690 // depMap is a map from a task ID to the tasks that depend on it 691 depMap := make(map[string][]task.Task) 692 // crossVariantTasks will contain all tasks with cross-variant dependencies 693 crossVariantTasks := make(map[string]task.Task) 694 for _, task := range tasks { 695 for _, dep := range task.DependsOn { 696 if taskPresent[dep.TaskId] { 697 depMap[dep.TaskId] = append(depMap[dep.TaskId], task) 698 } else { 699 crossVariantTasks[task.Id] = task 700 } 701 } 702 } 703 for id := range crossVariantTasks { 704 for _, task := range depMap[id] { 705 addDepChildren(task, crossVariantTasks, depMap) 706 } 707 } 708 // normalTasks will contain all tasks with no cross-variant dependencies 709 normalTasks := make(map[string]task.Task) 710 for _, t := range tasks { 711 if _, ok := crossVariantTasks[t.Id]; !ok { 712 normalTasks[t.Id] = t 713 } 714 } 715 716 // Construct a map of task Id to DisplayName, used to sort both sets of tasks 717 idToDisplayName := make(map[string]string) 718 for _, t := range tasks { 719 idToDisplayName[t.Id] = t.DisplayName 720 } 721 722 // All tasks with cross-variant dependencies appear to the right 723 sortedTasks := sortTasksHelper(normalTasks, idToDisplayName) 724 sortedTasks = append(sortedTasks, sortTasksHelper(crossVariantTasks, idToDisplayName)...) 725 return sortedTasks 726 } 727 728 // addDepChildren recursively adds task and all tasks depending on it to tasks 729 // depMap is a map from a task ID to the tasks that depend on it 730 func addDepChildren(task task.Task, tasks map[string]task.Task, depMap map[string][]task.Task) { 731 if _, ok := tasks[task.Id]; !ok { 732 tasks[task.Id] = task 733 for _, dep := range depMap[task.Id] { 734 addDepChildren(dep, tasks, depMap) 735 } 736 } 737 } 738 739 // sortTasksHelper sorts the tasks, assuming they all have cross-variant dependencies, or none have 740 // cross-variant dependencies 741 func sortTasksHelper(tasks map[string]task.Task, idToDisplayName map[string]string) []task.Task { 742 layers := layerTasks(tasks) 743 sortedTasks := make([]task.Task, 0, len(tasks)) 744 for _, layer := range layers { 745 sortedTasks = append(sortedTasks, sortLayer(layer, idToDisplayName)...) 746 } 747 return sortedTasks 748 } 749 750 // layerTasks sorts the tasks into layers 751 // Layer n contains all tasks whose dependencies are contained in layers 0 through n-1, or are not 752 // included in tasks (for tasks with cross-variant dependencies) 753 func layerTasks(tasks map[string]task.Task) [][]task.Task { 754 layers := make([][]task.Task, 0) 755 for len(tasks) > 0 { 756 // Create a new layer 757 layer := make([]task.Task, 0) 758 for _, task := range tasks { 759 // Check if all dependencies are included in previous layers (or were not in tasks) 760 if allDepsProcessed(task, tasks) { 761 layer = append(layer, task) 762 } 763 } 764 // Add current layer to list of layers 765 layers = append(layers, layer) 766 // Delete all tasks in this layer 767 for _, task := range layer { 768 delete(tasks, task.Id) 769 } 770 } 771 return layers 772 } 773 774 // allDepsProcessed checks whether any dependencies of task are in unprocessedTasks 775 func allDepsProcessed(task task.Task, unprocessedTasks map[string]task.Task) bool { 776 for _, dep := range task.DependsOn { 777 if _, unprocessed := unprocessedTasks[dep.TaskId]; unprocessed { 778 return false 779 } 780 } 781 return true 782 } 783 784 // sortLayer groups tasks by common dependencies, sorting alphabetically within each group 785 func sortLayer(layer []task.Task, idToDisplayName map[string]string) []task.Task { 786 sortKeys := make([]string, 0, len(layer)) 787 sortKeyToTask := make(map[string]task.Task) 788 for _, t := range layer { 789 // Construct a key to sort by, consisting of all dependency names, sorted alphabetically, 790 // followed by the task name 791 sortKeyWords := make([]string, 0, len(t.DependsOn)+1) 792 for _, dep := range t.DependsOn { 793 depName, ok := idToDisplayName[dep.TaskId] 794 // Cross-variant dependencies will not be included in idToDisplayName 795 if !ok { 796 depName = dep.TaskId 797 } 798 sortKeyWords = append(sortKeyWords, depName) 799 } 800 sort.Strings(sortKeyWords) 801 sortKeyWords = append(sortKeyWords, t.DisplayName) 802 sortKey := strings.Join(sortKeyWords, " ") 803 sortKeys = append(sortKeys, sortKey) 804 sortKeyToTask[sortKey] = t 805 } 806 sort.Strings(sortKeys) 807 sortedLayer := make([]task.Task, 0, len(layer)) 808 for _, sortKey := range sortKeys { 809 sortedLayer = append(sortedLayer, sortKeyToTask[sortKey]) 810 } 811 return sortedLayer 812 }