github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/task/task.go (about) 1 package task 2 3 import ( 4 "fmt" 5 "time" 6 7 "github.com/evergreen-ci/evergreen" 8 "github.com/evergreen-ci/evergreen/apimodels" 9 "github.com/evergreen-ci/evergreen/db" 10 "github.com/evergreen-ci/evergreen/model/event" 11 "github.com/evergreen-ci/evergreen/util" 12 "github.com/pkg/errors" 13 "gopkg.in/mgo.v2/bson" 14 ) 15 16 var ( 17 AgentHeartbeat = "heartbeat" 18 ) 19 20 type Task struct { 21 Id string `bson:"_id" json:"id"` 22 Secret string `bson:"secret" json:"secret"` 23 24 // time information for task 25 // create - the time we created this task in our database 26 // dispatch - the time the task runner starts up the agent on the host 27 // push - the time the commit generating this build was pushed to the remote 28 // scheduled - the time the commit is scheduled 29 // start - the time the agent starts the task on the host after spinning it up 30 // finish - the time the task was completed on the remote host 31 CreateTime time.Time `bson:"create_time" json:"create_time"` 32 DispatchTime time.Time `bson:"dispatch_time" json:"dispatch_time"` 33 PushTime time.Time `bson:"push_time" json:"push_time"` 34 ScheduledTime time.Time `bson:"scheduled_time" json:"scheduled_time"` 35 StartTime time.Time `bson:"start_time" json:"start_time"` 36 FinishTime time.Time `bson:"finish_time" json:"finish_time"` 37 38 Version string `bson:"version" json:"version,omitempty"` 39 Project string `bson:"branch" json:"branch,omitempty"` 40 Revision string `bson:"gitspec" json:"gitspec"` 41 Priority int64 `bson:"priority" json:"priority"` 42 43 // only relevant if the task is running. the time of the last heartbeat 44 // sent back by the agent 45 LastHeartbeat time.Time `bson:"last_heartbeat"` 46 47 // used to indicate whether task should be scheduled to run 48 Activated bool `bson:"activated" json:"activated"` 49 ActivatedBy string `bson:"activated_by" json:"activated_by"` 50 BuildId string `bson:"build_id" json:"build_id"` 51 DistroId string `bson:"distro" json:"distro"` 52 BuildVariant string `bson:"build_variant" json:"build_variant"` 53 DependsOn []Dependency `bson:"depends_on" json:"depends_on"` 54 NumDependents int `bson:"num_dependents,omitempty" json:"num_dependents,omitempty"` 55 56 // Human-readable name 57 DisplayName string `bson:"display_name" json:"display_name"` 58 59 // Tags that describe the task 60 Tags []string `bson:"tags,omitempty" json:"tags,omitempty"` 61 62 // The host the task was run on 63 HostId string `bson:"host_id" json:"host_id"` 64 65 // the number of times this task has been restarted 66 Restarts int `bson:"restarts" json:"restarts,omitempty"` 67 Execution int `bson:"execution" json:"execution"` 68 OldTaskId string `bson:"old_task_id,omitempty" json:"old_task_id,omitempty"` 69 Archived bool `bson:"archived,omitempty" json:"archived,omitempty"` 70 RevisionOrderNumber int `bson:"order,omitempty" json:"order,omitempty"` 71 72 // task requester - this is used to help tell the 73 // reason this task was created. e.g. it could be 74 // because the repotracker requested it (via tracking the 75 // repository) or it was triggered by a developer 76 // patch request 77 Requester string `bson:"r" json:"r"` 78 79 // Status represents the various stages the task could be in 80 Status string `bson:"status" json:"status"` 81 Details apimodels.TaskEndDetail `bson:"details" json:"task_end_details"` 82 Aborted bool `bson:"abort,omitempty" json:"abort"` 83 84 // TimeTaken is how long the task took to execute. meaningless if the task is not finished 85 TimeTaken time.Duration `bson:"time_taken" json:"time_taken"` 86 87 // how long we expect the task to take from start to finish 88 ExpectedDuration time.Duration `bson:"expected_duration,omitempty" json:"expected_duration,omitempty"` 89 90 // an estimate of what the task cost to run, hidden from JSON views for now 91 Cost float64 `bson:"cost,omitempty" json:"-"` 92 93 // test results captured and sent back by agent 94 TestResults []TestResult `bson:"test_results" json:"test_results"` 95 } 96 97 // Dependency represents a task that must be completed before the owning 98 // task can be scheduled. 99 type Dependency struct { 100 TaskId string `bson:"_id" json:"id"` 101 Status string `bson:"status" json:"status"` 102 } 103 104 // SetBSON allows us to use dependency representation of both 105 // just task Ids and of true Dependency structs. 106 // TODO eventually drop all of this switching 107 func (d *Dependency) SetBSON(raw bson.Raw) error { 108 // copy the Dependency type to remove this SetBSON method but preserve bson struct tags 109 type nakedDep Dependency 110 var depCopy nakedDep 111 if err := raw.Unmarshal(&depCopy); err == nil { 112 if depCopy.TaskId != "" { 113 *d = Dependency(depCopy) 114 return nil 115 } 116 } 117 118 // hack to support the legacy depends_on, since we can't just unmarshal a string 119 strBytes, _ := bson.Marshal(bson.RawD{{"str", raw}}) 120 var strStruct struct { 121 String string `bson:"str"` 122 } 123 if err := bson.Unmarshal(strBytes, &strStruct); err == nil { 124 if strStruct.String != "" { 125 d.TaskId = strStruct.String 126 d.Status = evergreen.TaskSucceeded 127 return nil 128 } 129 } 130 131 return bson.SetZero 132 } 133 134 // TestResults is only used when transferring data from agent to api. 135 type TestResults struct { 136 Results []TestResult `json:"results"` 137 } 138 139 type TestResult struct { 140 Status string `json:"status" bson:"status"` 141 TestFile string `json:"test_file" bson:"test_file"` 142 URL string `json:"url" bson:"url,omitempty"` 143 URLRaw string `json:"url_raw" bson:"url_raw,omitempty"` 144 LogId string `json:"log_id,omitempty" bson:"log_id,omitempty"` 145 LineNum int `json:"line_num,omitempty" bson:"line_num,omitempty"` 146 ExitCode int `json:"exit_code" bson:"exit_code"` 147 StartTime float64 `json:"start" bson:"start"` 148 EndTime float64 `json:"end" bson:"end"` 149 150 // LogRaw is not saved in the task 151 LogRaw string `json:"log_raw" bson:"log_raw,omitempty"` 152 } 153 154 var ( 155 AllStatuses = "*" 156 ) 157 158 // Abortable returns true if the task can be aborted. 159 func IsAbortable(t Task) bool { 160 return t.Status == evergreen.TaskStarted || 161 t.Status == evergreen.TaskDispatched 162 } 163 164 // IsFinished returns true if the project is no longer running 165 func IsFinished(t Task) bool { 166 return t.Status == evergreen.TaskFailed || 167 t.Status == evergreen.TaskSucceeded || 168 (t.Status == evergreen.TaskUndispatched && t.DispatchTime != util.ZeroTime) 169 } 170 171 // IsDispatchable return true if the task should be dispatched 172 func (t *Task) IsDispatchable() bool { 173 return t.Status == evergreen.TaskUndispatched && t.Activated 174 } 175 176 // satisfiesDependency checks a task the receiver task depends on 177 // to see if its status satisfies a dependency. If the "Status" field is 178 // unset, default to checking that is succeeded. 179 func (t *Task) satisfiesDependency(depTask *Task) bool { 180 for _, dep := range t.DependsOn { 181 if dep.TaskId == depTask.Id { 182 switch dep.Status { 183 case evergreen.TaskSucceeded, "": 184 return depTask.Status == evergreen.TaskSucceeded 185 case evergreen.TaskFailed: 186 return depTask.Status == evergreen.TaskFailed 187 case AllStatuses: 188 return depTask.Status == evergreen.TaskFailed || depTask.Status == evergreen.TaskSucceeded 189 } 190 } 191 } 192 return false 193 } 194 195 // Checks whether the dependencies for the task have all completed successfully. 196 // If any of the dependencies exist in the map that is passed in, they are 197 // used to check rather than fetching from the database. All queries 198 // are cached back into the map for later use. 199 func (t *Task) DependenciesMet(depCaches map[string]Task) (bool, error) { 200 201 if len(t.DependsOn) == 0 { 202 return true, nil 203 } 204 205 deps := make([]Task, 0, len(t.DependsOn)) 206 207 depIdsToQueryFor := make([]string, 0, len(t.DependsOn)) 208 for _, dep := range t.DependsOn { 209 if cachedDep, ok := depCaches[dep.TaskId]; !ok { 210 depIdsToQueryFor = append(depIdsToQueryFor, dep.TaskId) 211 } else { 212 deps = append(deps, cachedDep) 213 } 214 } 215 216 if len(depIdsToQueryFor) > 0 { 217 newDeps, err := Find(ByIds(depIdsToQueryFor).WithFields(StatusKey)) 218 if err != nil { 219 return false, err 220 } 221 222 // add queried dependencies to the cache 223 for _, newDep := range newDeps { 224 deps = append(deps, newDep) 225 depCaches[newDep.Id] = newDep 226 } 227 } 228 229 for _, depTask := range deps { 230 if !t.satisfiesDependency(&depTask) { 231 return false, nil 232 } 233 } 234 235 return true, nil 236 } 237 238 // UIStatus returns the status for this task that should be displayed in the 239 // UI. It uses a combination of the TaskEndDetails and the Task's status to 240 // determine the state of the task. 241 func (t *Task) UIStatus() string { 242 status := t.Status 243 if t.Status == evergreen.TaskUndispatched { 244 if !t.Activated { 245 status = evergreen.TaskInactive 246 } else { 247 status = "unstarted" 248 } 249 } else if t.Status == evergreen.TaskStarted { 250 status = evergreen.TaskStarted 251 } else if t.Status == evergreen.TaskSucceeded { 252 status = evergreen.TaskSucceeded 253 } else if t.Status == evergreen.TaskFailed { 254 status = evergreen.TaskFailed 255 if t.Details.Type == "system" { 256 status = evergreen.TaskSystemFailed 257 if t.Details.TimedOut { 258 if t.Details.Description == "heartbeat" { 259 status = evergreen.TaskSystemUnresponse 260 } 261 if t.Details.Type == "system" { 262 status = evergreen.TaskSystemTimedOut 263 } 264 status = evergreen.TaskTestTimedOut 265 } 266 } 267 } 268 return status 269 } 270 271 // FindTaskOnBaseCommit returns the task that is on the base commit. 272 func (t *Task) FindTaskOnBaseCommit() (*Task, error) { 273 return FindOne(ByCommit(t.Revision, t.BuildVariant, t.DisplayName, t.Project, evergreen.RepotrackerVersionRequester)) 274 } 275 276 // FindIntermediateTasks returns the tasks from most recent to least recent between two tasks. 277 func (current *Task) FindIntermediateTasks(previous *Task) ([]Task, error) { 278 intermediateTasks, err := Find(ByIntermediateRevisions(previous.RevisionOrderNumber, current.RevisionOrderNumber, current.BuildVariant, 279 current.DisplayName, current.Project, current.Requester)) 280 if err != nil { 281 return nil, err 282 } 283 284 // reverse the slice of tasks 285 intermediateTasksReversed := make([]Task, len(intermediateTasks)) 286 for idx, t := range intermediateTasks { 287 intermediateTasksReversed[len(intermediateTasks)-idx-1] = t 288 } 289 return intermediateTasksReversed, nil 290 } 291 292 // CountSimilarFailingTasks returns a count of all tasks with the same project, 293 // same display name, and in other buildvariants, that have failed in the same 294 // revision 295 func (t *Task) CountSimilarFailingTasks() (int, error) { 296 return Count(ByDifferentFailedBuildVariants(t.Revision, t.BuildVariant, t.DisplayName, 297 t.Project, t.Requester)) 298 } 299 300 // Find the previously completed task for the same requester + project + 301 // build variant + display name combination as the specified task 302 func (t *Task) PreviousCompletedTask(project string, 303 statuses []string) (*Task, error) { 304 if len(statuses) == 0 { 305 statuses = CompletedStatuses 306 } 307 return FindOne(ByBeforeRevisionWithStatuses(t.RevisionOrderNumber, statuses, t.BuildVariant, 308 t.DisplayName, project)) 309 } 310 311 // SetExpectedDuration updates the expected duration field for the task 312 func (t *Task) SetExpectedDuration(duration time.Duration) error { 313 return UpdateOne( 314 bson.M{ 315 IdKey: t.Id, 316 }, 317 bson.M{ 318 "$set": bson.M{ 319 ExpectedDurationKey: duration, 320 }, 321 }, 322 ) 323 } 324 325 // Mark that the task has been dispatched onto a particular host. Sets the 326 // running task field on the host and the host id field on the task. 327 // Returns an error if any of the database updates fail. 328 func (t *Task) MarkAsDispatched(hostId string, distroId string, dispatchTime time.Time) error { 329 t.DispatchTime = dispatchTime 330 t.Status = evergreen.TaskDispatched 331 t.HostId = hostId 332 t.LastHeartbeat = dispatchTime 333 t.DistroId = distroId 334 return UpdateOne( 335 bson.M{ 336 IdKey: t.Id, 337 }, 338 bson.M{ 339 "$set": bson.M{ 340 DispatchTimeKey: dispatchTime, 341 StatusKey: evergreen.TaskDispatched, 342 HostIdKey: hostId, 343 LastHeartbeatKey: dispatchTime, 344 DistroIdKey: distroId, 345 }, 346 "$unset": bson.M{ 347 AbortedKey: "", 348 TestResultsKey: "", 349 DetailsKey: "", 350 }, 351 }, 352 ) 353 354 } 355 356 // MarkAsUndispatched marks that the task has been undispatched from a 357 // particular host. Unsets the running task field on the host and the 358 // host id field on the task 359 // Returns an error if any of the database updates fail. 360 func (t *Task) MarkAsUndispatched() error { 361 // then, update the task document 362 t.Status = evergreen.TaskUndispatched 363 364 return UpdateOne( 365 bson.M{ 366 IdKey: t.Id, 367 }, 368 bson.M{ 369 "$set": bson.M{ 370 StatusKey: evergreen.TaskUndispatched, 371 }, 372 "$unset": bson.M{ 373 DispatchTimeKey: util.ZeroTime, 374 LastHeartbeatKey: util.ZeroTime, 375 DistroIdKey: "", 376 HostIdKey: "", 377 AbortedKey: "", 378 TestResultsKey: "", 379 DetailsKey: "", 380 }, 381 }, 382 ) 383 } 384 385 // SetTasksScheduledTime takes a list of tasks and a time, and then sets 386 // the scheduled time in the database for the tasks if it is currently unset 387 func SetTasksScheduledTime(tasks []Task, scheduledTime time.Time) error { 388 var ids []string 389 for i := range tasks { 390 tasks[i].ScheduledTime = scheduledTime 391 ids = append(ids, tasks[i].Id) 392 } 393 info, err := UpdateAll( 394 bson.M{ 395 IdKey: bson.M{ 396 "$in": ids, 397 }, 398 ScheduledTimeKey: bson.M{ 399 "$lte": util.ZeroTime, 400 }, 401 }, 402 bson.M{ 403 "$set": bson.M{ 404 ScheduledTimeKey: scheduledTime, 405 }, 406 }, 407 ) 408 if err != nil { 409 return err 410 } 411 412 if info.Updated > 0 { 413 for _, t := range tasks { 414 event.LogTaskScheduled(t.Id, scheduledTime) 415 } 416 } 417 return nil 418 419 } 420 421 // MarkFailed changes the state of the task to failed. 422 func (t *Task) MarkFailed() error { 423 t.Status = evergreen.TaskFailed 424 return UpdateOne( 425 bson.M{ 426 IdKey: t.Id, 427 }, 428 bson.M{ 429 "$set": bson.M{ 430 StatusKey: evergreen.TaskFailed, 431 }, 432 }, 433 ) 434 } 435 436 // SetAborted sets the abort field of task to aborted 437 func (t *Task) SetAborted() error { 438 t.Aborted = true 439 return UpdateOne( 440 bson.M{ 441 IdKey: t.Id, 442 }, 443 bson.M{ 444 "$set": bson.M{ 445 AbortedKey: true, 446 }, 447 }, 448 ) 449 } 450 451 // ActivateTask will set the ActivatedBy field to the caller and set the active state to be true 452 func (t *Task) ActivateTask(caller string) error { 453 t.ActivatedBy = caller 454 t.Activated = true 455 return UpdateOne(bson.M{ 456 IdKey: t.Id, 457 }, 458 bson.M{ 459 "$set": bson.M{ 460 ActivatedKey: true, 461 ActivatedByKey: caller, 462 }, 463 }) 464 } 465 466 // DeactivateTask will set the ActivatedBy field to the caller and set the active state to be false and deschedule the task 467 func (t *Task) DeactivateTask(caller string) error { 468 t.ActivatedBy = caller 469 t.Activated = false 470 t.ScheduledTime = util.ZeroTime 471 return UpdateOne( 472 bson.M{ 473 IdKey: t.Id, 474 }, 475 bson.M{ 476 "$set": bson.M{ 477 ActivatedKey: false, 478 ScheduledTimeKey: util.ZeroTime, 479 }, 480 }, 481 ) 482 } 483 484 // MarkEnd handles the Task updates associated with ending a task. If the task's start time is zero 485 // at this time, it will set it to the finish time minus the timeout time. 486 func (t *Task) MarkEnd(finishTime time.Time, detail *apimodels.TaskEndDetail) error { 487 // record that the task has finished, in memory and in the db 488 t.Status = detail.Status 489 t.FinishTime = finishTime 490 491 // if there is no start time set, either set it to the create time 492 // or set 2 hours previous to the finish time. 493 if util.IsZeroTime(t.StartTime) { 494 timedOutStart := finishTime.Add(-2 * time.Hour) 495 t.StartTime = timedOutStart 496 if timedOutStart.Before(t.CreateTime) { 497 t.StartTime = t.CreateTime 498 } 499 } 500 501 t.TimeTaken = finishTime.Sub(t.StartTime) 502 t.Details = *detail 503 return UpdateOne( 504 bson.M{ 505 IdKey: t.Id, 506 }, 507 bson.M{ 508 "$set": bson.M{ 509 FinishTimeKey: finishTime, 510 StatusKey: detail.Status, 511 TimeTakenKey: t.TimeTaken, 512 DetailsKey: t.Details, 513 StartTimeKey: t.StartTime, 514 }, 515 "$unset": bson.M{ 516 AbortedKey: "", 517 }, 518 }) 519 520 } 521 522 // Reset sets the task state to be activated, with a new secret, 523 // undispatched status and zero time on Start, Scheduled, Dispatch and FinishTime 524 func (t *Task) Reset() error { 525 t.Activated = true 526 t.Secret = util.RandomString() 527 t.DispatchTime = util.ZeroTime 528 t.StartTime = util.ZeroTime 529 t.ScheduledTime = util.ZeroTime 530 t.FinishTime = util.ZeroTime 531 t.TestResults = []TestResult{} 532 reset := bson.M{ 533 "$set": bson.M{ 534 ActivatedKey: true, 535 SecretKey: t.Secret, 536 StatusKey: evergreen.TaskUndispatched, 537 DispatchTimeKey: util.ZeroTime, 538 StartTimeKey: util.ZeroTime, 539 ScheduledTimeKey: util.ZeroTime, 540 FinishTimeKey: util.ZeroTime, 541 TestResultsKey: []TestResult{}, 542 }, 543 "$unset": bson.M{ 544 DetailsKey: "", 545 }, 546 } 547 548 return UpdateOne( 549 bson.M{ 550 IdKey: t.Id, 551 }, 552 reset, 553 ) 554 } 555 556 // Reset sets the task state to be activated, with a new secret, 557 // undispatched status and zero time on Start, Scheduled, Dispatch and FinishTime 558 func ResetTasks(taskIds []string) error { 559 reset := bson.M{ 560 "$set": bson.M{ 561 ActivatedKey: true, 562 SecretKey: util.RandomString(), 563 StatusKey: evergreen.TaskUndispatched, 564 DispatchTimeKey: util.ZeroTime, 565 StartTimeKey: util.ZeroTime, 566 ScheduledTimeKey: util.ZeroTime, 567 FinishTimeKey: util.ZeroTime, 568 TestResultsKey: []TestResult{}, 569 }, 570 "$unset": bson.M{ 571 DetailsKey: "", 572 }, 573 } 574 575 _, err := UpdateAll( 576 bson.M{ 577 IdKey: bson.M{"$in": taskIds}, 578 }, 579 reset, 580 ) 581 return err 582 583 } 584 585 // UpdateHeartbeat updates the heartbeat to be the current time 586 func (t *Task) UpdateHeartbeat() error { 587 t.LastHeartbeat = time.Now() 588 return UpdateOne( 589 bson.M{ 590 IdKey: t.Id, 591 }, 592 bson.M{ 593 "$set": bson.M{ 594 LastHeartbeatKey: t.LastHeartbeat, 595 }, 596 }, 597 ) 598 } 599 600 // SetPriority sets the priority of the tasks and the tasks that they depend on 601 func (t *Task) SetPriority(priority int64) error { 602 t.Priority = priority 603 modifier := bson.M{PriorityKey: priority} 604 605 //blacklisted - this task should never run, so unschedule it now 606 if priority < 0 { 607 modifier[ActivatedKey] = false 608 } 609 610 ids, err := t.getRecursiveDependencies() 611 if err != nil { 612 return errors.Wrap(err, "error getting task dependencies") 613 } 614 615 _, err = UpdateAll( 616 bson.M{"$or": []bson.M{ 617 {IdKey: t.Id}, 618 {IdKey: bson.M{"$in": ids}, 619 PriorityKey: bson.M{"$lt": priority}}, 620 }}, 621 bson.M{"$set": modifier}, 622 ) 623 return errors.WithStack(err) 624 } 625 626 // getRecursiveDependencies creates a slice containing t.Id and the Ids of all recursive dependencies. 627 // We assume there are no dependency cycles. 628 func (t *Task) getRecursiveDependencies() ([]string, error) { 629 recurIds := make([]string, 0, len(t.DependsOn)) 630 for _, dependency := range t.DependsOn { 631 recurIds = append(recurIds, dependency.TaskId) 632 } 633 634 recurTasks, err := Find(ByIds(recurIds)) 635 if err != nil { 636 return nil, errors.WithStack(err) 637 } 638 639 ids := make([]string, 0) 640 for _, recurTask := range recurTasks { 641 appendIds, err := recurTask.getRecursiveDependencies() 642 if err != nil { 643 return nil, errors.WithStack(err) 644 } 645 ids = append(ids, appendIds...) 646 } 647 648 ids = append(ids, t.Id) 649 return ids, nil 650 } 651 652 // MarkStart updates the task's start time and sets the status to started 653 func (t *Task) MarkStart(startTime time.Time) error { 654 // record the start time in the in-memory task 655 t.StartTime = startTime 656 t.Status = evergreen.TaskStarted 657 return UpdateOne( 658 bson.M{ 659 IdKey: t.Id, 660 }, 661 bson.M{ 662 "$set": bson.M{ 663 StatusKey: evergreen.TaskStarted, 664 StartTimeKey: startTime, 665 }, 666 }, 667 ) 668 } 669 670 // SetResults sets the results of the task in TestResults 671 func (t *Task) SetResults(results []TestResult) error { 672 t.TestResults = results 673 return UpdateOne( 674 bson.M{ 675 IdKey: t.Id, 676 }, 677 bson.M{ 678 "$set": bson.M{ 679 TestResultsKey: results, 680 }, 681 }, 682 ) 683 } 684 685 // MarkUnscheduled marks the task as undispatched and updates it in the database 686 func (t *Task) MarkUnscheduled() error { 687 t.Status = evergreen.TaskUndispatched 688 return UpdateOne( 689 bson.M{ 690 IdKey: t.Id, 691 }, 692 bson.M{ 693 "$set": bson.M{ 694 StatusKey: evergreen.TaskUndispatched, 695 }, 696 }, 697 ) 698 699 } 700 701 // ClearResults sets the TestResults to an empty list 702 func (t *Task) ClearResults() error { 703 t.TestResults = []TestResult{} 704 return UpdateOne( 705 bson.M{ 706 IdKey: t.Id, 707 }, 708 bson.M{ 709 "$set": bson.M{ 710 TestResultsKey: []TestResult{}, 711 }, 712 }, 713 ) 714 } 715 716 // SetCost updates the task's Cost field 717 func (t *Task) SetCost(cost float64) error { 718 t.Cost = cost 719 return UpdateOne( 720 bson.M{ 721 IdKey: t.Id, 722 }, 723 bson.M{ 724 "$set": bson.M{ 725 CostKey: cost, 726 }, 727 }, 728 ) 729 } 730 731 // AbortBuild sets the abort flag on all tasks associated with the build which are in an abortable 732 // state 733 func AbortBuild(buildId string) error { 734 _, err := UpdateAll( 735 bson.M{ 736 BuildIdKey: buildId, 737 StatusKey: bson.M{"$in": evergreen.AbortableStatuses}, 738 }, 739 bson.M{"$set": bson.M{AbortedKey: true}}, 740 ) 741 return errors.WithStack(err) 742 } 743 744 //String represents the stringified version of a task 745 func (t *Task) String() (taskStruct string) { 746 taskStruct += fmt.Sprintf("Id: %v\n", t.Id) 747 taskStruct += fmt.Sprintf("Status: %v\n", t.Status) 748 taskStruct += fmt.Sprintf("Host: %v\n", t.HostId) 749 taskStruct += fmt.Sprintf("ScheduledTime: %v\n", t.ScheduledTime) 750 taskStruct += fmt.Sprintf("DispatchTime: %v\n", t.DispatchTime) 751 taskStruct += fmt.Sprintf("StartTime: %v\n", t.StartTime) 752 taskStruct += fmt.Sprintf("FinishTime: %v\n", t.FinishTime) 753 taskStruct += fmt.Sprintf("TimeTaken: %v\n", t.TimeTaken) 754 taskStruct += fmt.Sprintf("Activated: %v\n", t.Activated) 755 taskStruct += fmt.Sprintf("Requester: %v\n", t.FinishTime) 756 return 757 } 758 759 // Insert writes the b to the db. 760 func (t *Task) Insert() error { 761 return db.Insert(Collection, t) 762 } 763 764 // Inserts the task into the old_tasks collection 765 func (t *Task) Archive() error { 766 var update bson.M 767 768 // only increment restarts if have a current restarts 769 // this way restarts will never be set for new tasks but will be 770 // maintained for old ones 771 if t.Restarts > 0 { 772 update = bson.M{"$inc": bson.M{ 773 ExecutionKey: 1, 774 RestartsKey: 1, 775 }} 776 } else { 777 update = bson.M{ 778 "$inc": bson.M{ExecutionKey: 1}, 779 } 780 } 781 err := UpdateOne( 782 bson.M{IdKey: t.Id}, 783 update) 784 if err != nil { 785 return errors.Wrap(err, "task.Archive() failed") 786 } 787 archiveTask := *t 788 archiveTask.Id = fmt.Sprintf("%v_%v", t.Id, t.Execution) 789 archiveTask.OldTaskId = t.Id 790 archiveTask.Archived = true 791 err = db.Insert(OldCollection, &archiveTask) 792 if err != nil { 793 return errors.Wrap(err, "task.Archive() failed") 794 } 795 return nil 796 } 797 798 // Aggregation 799 800 // AverageTaskTimeDifference takes two field names (such that field2 happened 801 // after field1), a field to group on, and a cutoff time. 802 // It returns the average duration between fields 1 and 2, grouped by 803 // the groupBy field, including only task documents where both time 804 // fields happened after the given cutoff time. This information is returned 805 // as a map from groupBy_field -> avg_time_difference 806 // 807 // NOTE: THIS FUNCTION DOES NOT SANITIZE INPUT! 808 // BAD THINGS CAN HAPPEN IF NON-TIME FIELDNAMES ARE PASSED IN 809 // OR IF A FIELD OF NON-STRING TYPE IS SUPPLIED FOR groupBy! 810 func AverageTaskTimeDifference(field1 string, field2 string, 811 groupByField string, cutoff time.Time) (map[string]time.Duration, error) { 812 813 // This pipeline returns the average time difference between 814 // two time fields, grouped by a given field of "string" type. 815 // It assumes field2 happened later than field1. 816 // Time difference returned in milliseconds. 817 pipeline := []bson.M{ 818 {"$match": bson.M{ 819 field1: bson.M{"$gt": cutoff}, 820 field2: bson.M{"$gt": cutoff}}}, 821 {"$group": bson.M{ 822 "_id": "$" + groupByField, 823 "avg_time": bson.M{ 824 "$avg": bson.M{ 825 "$subtract": []string{"$" + field2, "$" + field1}, 826 }, 827 }, 828 }}, 829 } 830 831 // anonymous struct for unmarshalling result bson 832 // NOTE: This means we can only group by string fields currently 833 var results []struct { 834 GroupId string `bson:"_id"` 835 AverageTime int64 `bson:"avg_time"` 836 } 837 838 err := db.Aggregate(Collection, pipeline, &results) 839 if err != nil { 840 return nil, errors.Wrapf(err, "Error aggregating task times by [%v, %v]", field1, field2) 841 } 842 843 avgTimes := make(map[string]time.Duration) 844 for _, res := range results { 845 avgTimes[res.GroupId] = time.Duration(res.AverageTime) * time.Millisecond 846 } 847 848 return avgTimes, nil 849 } 850 851 // ExpectedTaskDuration takes a given project and buildvariant and computes 852 // the average duration - grouped by task display name - for tasks that have 853 // completed within a given threshold as determined by the window 854 func ExpectedTaskDuration(project, buildvariant string, window time.Duration) (map[string]time.Duration, error) { 855 pipeline := []bson.M{ 856 { 857 "$match": bson.M{ 858 BuildVariantKey: buildvariant, 859 ProjectKey: project, 860 StatusKey: bson.M{ 861 "$in": []string{evergreen.TaskSucceeded, evergreen.TaskFailed}, 862 }, 863 DetailsKey + "." + TaskEndDetailTimedOut: bson.M{ 864 "$ne": true, 865 }, 866 FinishTimeKey: bson.M{ 867 "$gte": time.Now().Add(-window), 868 }, 869 StartTimeKey: bson.M{ 870 // make sure all documents have a valid start time so we don't 871 // return tasks with runtimes of multiple years 872 "$gt": util.ZeroTime, 873 }, 874 }, 875 }, 876 { 877 "$project": bson.M{ 878 DisplayNameKey: 1, 879 TimeTakenKey: 1, 880 IdKey: 0, 881 }, 882 }, 883 { 884 "$group": bson.M{ 885 "_id": fmt.Sprintf("$%v", DisplayNameKey), 886 "exp_dur": bson.M{ 887 "$avg": fmt.Sprintf("$%v", TimeTakenKey), 888 }, 889 }, 890 }, 891 } 892 893 // anonymous struct for unmarshalling result bson 894 var results []struct { 895 DisplayName string `bson:"_id"` 896 ExpectedDuration int64 `bson:"exp_dur"` 897 } 898 899 err := db.Aggregate(Collection, pipeline, &results) 900 if err != nil { 901 return nil, errors.Wrap(err, "error aggregating task average duration") 902 } 903 904 expDurations := make(map[string]time.Duration) 905 for _, result := range results { 906 expDuration := time.Duration(result.ExpectedDuration) * time.Nanosecond 907 expDurations[result.DisplayName] = expDuration 908 } 909 910 return expDurations, nil 911 }