/
task.go
3686 lines (3293 loc) · 118 KB
/
task.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package task
import (
"context"
"fmt"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/evergreen-ci/evergreen"
"github.com/evergreen-ci/evergreen/apimodels"
"github.com/evergreen-ci/evergreen/db"
mgobson "github.com/evergreen-ci/evergreen/db/mgo/bson"
"github.com/evergreen-ci/evergreen/model/distro"
"github.com/evergreen-ci/evergreen/model/event"
"github.com/evergreen-ci/evergreen/model/log"
"github.com/evergreen-ci/evergreen/model/testresult"
"github.com/evergreen-ci/evergreen/taskoutput"
"github.com/evergreen-ci/evergreen/util"
"github.com/evergreen-ci/tarjan"
"github.com/evergreen-ci/utility"
"github.com/mongodb/anser/bsonutil"
adb "github.com/mongodb/anser/db"
"github.com/mongodb/grip"
"github.com/mongodb/grip/message"
"github.com/mongodb/grip/recovery"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo"
)
const (
dependencyKey = "dependencies"
// UnschedulableThreshold is the threshold after which a task waiting to
// dispatch should be unscheduled due to staleness.
UnschedulableThreshold = 7 * 24 * time.Hour
// indicates the window of completed tasks we want to use in computing
// average task duration. By default we use tasks that have
// completed within the last 7 days
taskCompletionEstimateWindow = 24 * 7 * time.Hour
// if we have no data on a given task, default to 10 minutes so we
// have some new hosts spawned
defaultTaskDuration = 10 * time.Minute
// length of time to cache the expected duration in the task document
predictionTTL = 8 * time.Hour
)
var (
// A regex that matches either / or \ for splitting directory paths
// on either windows or linux paths.
eitherSlash = regexp.MustCompile(`[/\\]`)
)
type Task struct {
Id string `bson:"_id" json:"id"`
Secret string `bson:"secret" json:"secret"`
// time information for task
// CreateTime - the creation time for the task, derived from the commit time or the patch creation time.
// DispatchTime - the time the task runner starts up the agent on the host.
// ScheduledTime - the time the task is scheduled.
// StartTime - the time the agent starts the task on the host after spinning it up.
// FinishTime - the time the task was completed on the remote host.
// ActivatedTime - the time the task was marked as available to be scheduled, automatically or by a developer.
// DependenciesMet - for tasks that have dependencies, the time all dependencies are met.
// ContainerAllocated - for tasks that run on containers, the time the container was allocated.
CreateTime time.Time `bson:"create_time" json:"create_time"`
IngestTime time.Time `bson:"injest_time" json:"ingest_time"`
DispatchTime time.Time `bson:"dispatch_time" json:"dispatch_time"`
ScheduledTime time.Time `bson:"scheduled_time" json:"scheduled_time"`
StartTime time.Time `bson:"start_time" json:"start_time"`
FinishTime time.Time `bson:"finish_time" json:"finish_time"`
ActivatedTime time.Time `bson:"activated_time" json:"activated_time"`
DependenciesMetTime time.Time `bson:"dependencies_met_time,omitempty" json:"dependencies_met_time,omitempty"`
ContainerAllocatedTime time.Time `bson:"container_allocated_time,omitempty" json:"container_allocated_time,omitempty"`
Version string `bson:"version" json:"version,omitempty"`
Project string `bson:"branch" json:"branch,omitempty"`
Revision string `bson:"gitspec" json:"gitspec"`
Priority int64 `bson:"priority" json:"priority"`
TaskGroup string `bson:"task_group" json:"task_group"`
TaskGroupMaxHosts int `bson:"task_group_max_hosts,omitempty" json:"task_group_max_hosts,omitempty"`
TaskGroupOrder int `bson:"task_group_order,omitempty" json:"task_group_order,omitempty"`
ResultsService string `bson:"results_service,omitempty" json:"results_service,omitempty"`
HasCedarResults bool `bson:"has_cedar_results,omitempty" json:"has_cedar_results,omitempty"`
ResultsFailed bool `bson:"results_failed,omitempty" json:"results_failed,omitempty"`
MustHaveResults bool `bson:"must_have_results,omitempty" json:"must_have_results,omitempty"`
// only relevant if the task is running. the time of the last heartbeat
// sent back by the agent
LastHeartbeat time.Time `bson:"last_heartbeat" json:"last_heartbeat"`
// Activated indicates whether the task should be scheduled to run or not.
Activated bool `bson:"activated" json:"activated"`
ActivatedBy string `bson:"activated_by" json:"activated_by"`
DeactivatedForDependency bool `bson:"deactivated_for_dependency" json:"deactivated_for_dependency"`
// ContainerAllocated indicates whether this task has been allocated a
// container to run it. It only applies to tasks running in containers.
ContainerAllocated bool `bson:"container_allocated" json:"container_allocated"`
// ContainerAllocationAttempts is the number of times this task has
// been allocated a container to run it (for a single execution).
ContainerAllocationAttempts int `bson:"container_allocation_attempts" json:"container_allocation_attempts"`
BuildId string `bson:"build_id" json:"build_id"`
DistroId string `bson:"distro" json:"distro"`
// Container is the name of the container configuration for running a
// container task.
Container string `bson:"container,omitempty" json:"container,omitempty"`
// ContainerOpts contains the options to configure the container that will
// run the task.
ContainerOpts ContainerOptions `bson:"container_options,omitempty" json:"container_options,omitempty"`
BuildVariant string `bson:"build_variant" json:"build_variant"`
BuildVariantDisplayName string `bson:"build_variant_display_name" json:"-"`
DependsOn []Dependency `bson:"depends_on" json:"depends_on"`
// UnattainableDependency caches the contents of DependsOn for more efficient querying.
UnattainableDependency bool `bson:"unattainable_dependency" json:"unattainable_dependency"`
NumDependents int `bson:"num_dependents,omitempty" json:"num_dependents,omitempty"`
// OverrideDependencies indicates whether a task should override its dependencies. If set, it will not
// wait for its dependencies to finish before running.
OverrideDependencies bool `bson:"override_dependencies,omitempty" json:"override_dependencies,omitempty"`
// SecondaryDistros refer to the optional secondary distros that can be
// associated with a task. This is used for running tasks in case there are
// idle hosts in a distro with an empty primary queue. This is a distinct concept
// from distro aliases (i.e. alternative distro names).
// Tags refer to outdated naming; maintained for compatibility.
SecondaryDistros []string `bson:"distro_aliases,omitempty" json:"distro_aliases,omitempty"`
// Human-readable name
DisplayName string `bson:"display_name" json:"display_name"`
// Tags that describe the task
Tags []string `bson:"tags,omitempty" json:"tags,omitempty"`
// The host the task was run on. This value is only set for host tasks.
HostId string `bson:"host_id,omitempty" json:"host_id"`
// PodID is the pod that was assigned to run the task. This value is only
// set for container tasks.
PodID string `bson:"pod_id,omitempty" json:"pod_id"`
// ExecutionPlatform determines the execution environment that the task runs
// in.
ExecutionPlatform ExecutionPlatform `bson:"execution_platform,omitempty" json:"execution_platform,omitempty"`
// The version of the agent this task was run on.
AgentVersion string `bson:"agent_version,omitempty" json:"agent_version,omitempty"`
// TaskOutputInfo holds the information for the interface that
// coordinates persistent storage of a task's output data.
// There are four possible scenarios:
// 1. The task will never have output data (e.g., display tasks)
// and, therefore, the value is and always will be nil.
// 2. The task does not have output data yet, but can in the future
// if/when dispatched, and, therefore, the value is currently
// nil.
// 3. The task has been dispatched with the task output information
// initialized and the application can safely use this field to
// fetch any output data. If the task has not finished running,
// the output data is accessible but may not be complete yet.
// 4. The task has data but was run before the introduction of the
// field and should be initialized before the application can
// safely fetch any output data.
// This field should *never* be accessed directly, instead call
// `Task.getTaskOutputSafe()`.
TaskOutputInfo *taskoutput.TaskOutput `bson:"task_output_info,omitempty" json:"task_output_info,omitempty"`
// Set to true if the task should be considered for mainline github checks
IsGithubCheck bool `bson:"is_github_check,omitempty" json:"is_github_check,omitempty"`
// CheckRunPath is a local file path to an output json file for the checkrun.
CheckRunPath *string `bson:"check_run_path,omitempty" json:"check_run_path,omitempty"`
// CheckRunId is the id for the checkrun that was created in github.
// This is used to update the checkrun for future executions of the task.
CheckRunId *int64 `bson:"check_run_id,omitempty" json:"check_run_id,omitempty"`
// CanReset indicates that the task has successfully archived and is in a valid state to be reset.
CanReset bool `bson:"can_reset,omitempty" json:"can_reset,omitempty"`
Execution int `bson:"execution" json:"execution"`
OldTaskId string `bson:"old_task_id,omitempty" json:"old_task_id,omitempty"`
Archived bool `bson:"archived,omitempty" json:"archived,omitempty"`
// RevisionOrderNumber for user-submitted patches is the user's current patch submission count.
// For mainline commits for a project, it is the amount of versions for that repositry so far.
RevisionOrderNumber int `bson:"order,omitempty" json:"order,omitempty"`
// task requester - this is used to help tell the
// reason this task was created. e.g. it could be
// because the repotracker requested it (via tracking the
// repository) or it was triggered by a developer
// patch request
Requester string `bson:"r" json:"r"`
// tasks that are part of a child patch will store the id and patch number of the parent patch
ParentPatchID string `bson:"parent_patch_id,omitempty" json:"parent_patch_id,omitempty"`
ParentPatchNumber int `bson:"parent_patch_number,omitempty" json:"parent_patch_number,omitempty"`
// Status represents the various stages the task could be in. Note that this
// task status is distinct from the way a task status is displayed in the
// UI. For example, a task that has failed will have a status of
// evergreen.TaskFailed regardless of the specific cause of failure.
// However, in the UI, the displayed status supports more granular failure
// type such as system failed and setup failed by checking this status and
// the task status details.
Status string `bson:"status" json:"status"`
Details apimodels.TaskEndDetail `bson:"details" json:"task_end_details"`
Aborted bool `bson:"abort,omitempty" json:"abort"`
AbortInfo AbortInfo `bson:"abort_info,omitempty" json:"abort_info,omitempty"`
// HostCreateDetails stores information about why host.create failed for this task
HostCreateDetails []HostCreateDetail `bson:"host_create_details,omitempty" json:"host_create_details,omitempty"`
// DisplayStatus is not persisted to the db. It is the status to display in the UI.
// It may be added via aggregation
DisplayStatus string `bson:"display_status,omitempty" json:"display_status,omitempty"`
// BaseTask is not persisted to the db. It is the data of the task on the base commit
// It may be added via aggregation
BaseTask BaseTaskInfo `bson:"base_task" json:"base_task"`
// TimeTaken is how long the task took to execute (if it has finished) or how long the task has been running (if it has started)
TimeTaken time.Duration `bson:"time_taken" json:"time_taken"`
// WaitSinceDependenciesMet is populated in GetDistroQueueInfo, used for host allocation
WaitSinceDependenciesMet time.Duration `bson:"wait_since_dependencies_met,omitempty" json:"wait_since_dependencies_met,omitempty"`
// how long we expect the task to take from start to
// finish. expected duration is the legacy value, but the UI
// probably depends on it, so we maintain both values.
ExpectedDuration time.Duration `bson:"expected_duration,omitempty" json:"expected_duration,omitempty"`
ExpectedDurationStdDev time.Duration `bson:"expected_duration_std_dev,omitempty" json:"expected_duration_std_dev,omitempty"`
DurationPrediction util.CachedDurationValue `bson:"duration_prediction,omitempty" json:"-"`
// test results embedded from the testresults collection
LocalTestResults []testresult.TestResult `bson:"-" json:"test_results"`
// display task fields
DisplayOnly bool `bson:"display_only,omitempty" json:"display_only,omitempty"`
ExecutionTasks []string `bson:"execution_tasks,omitempty" json:"execution_tasks,omitempty"`
LatestParentExecution int `bson:"latest_parent_execution" json:"latest_parent_execution"`
StepbackInfo *StepbackInfo `bson:"stepback_info,omitempty" json:"stepback_info,omitempty"`
// ResetWhenFinished indicates that a task should be reset once it is
// finished running. This is typically to deal with tasks that should be
// reset but cannot do so yet because they're currently running.
ResetWhenFinished bool `bson:"reset_when_finished,omitempty" json:"reset_when_finished,omitempty"`
ResetFailedWhenFinished bool `bson:"reset_failed_when_finished,omitempty" json:"reset_failed_when_finished,omitempty"`
// NumAutomaticRestarts is the number of times the task has been programmatically restarted via a failed agent command.
NumAutomaticRestarts int `bson:"num_automatic_restarts,omitempty" json:"num_automatic_restarts,omitempty"`
// IsAutomaticRestart indicates that the task was restarted via a failing agent command that was set to retry on failure.
IsAutomaticRestart bool `bson:"is_automatic_restart,omitempty" json:"is_automatic_restart,omitempty"`
DisplayTask *Task `bson:"-" json:"-"` // this is a local pointer from an exec to display task
// DisplayTaskId is set to the display task ID if the task is an execution task, the empty string if it's not an execution task,
// and is nil if we haven't yet checked whether or not this task has a display task.
DisplayTaskId *string `bson:"display_task_id,omitempty" json:"display_task_id,omitempty"`
// GenerateTask indicates that the task generates other tasks, which the
// scheduler will use to prioritize this task. This will not be set for
// tasks where the generate.tasks command runs outside of the main task
// block (e.g. pre, timeout).
GenerateTask bool `bson:"generate_task,omitempty" json:"generate_task,omitempty"`
// GeneratedTasks indicates that the task has already generated other tasks. This fields
// allows us to noop future requests, since a task should only generate others once.
GeneratedTasks bool `bson:"generated_tasks,omitempty" json:"generated_tasks,omitempty"`
// GeneratedBy, if present, is the ID of the task that generated this task.
GeneratedBy string `bson:"generated_by,omitempty" json:"generated_by,omitempty"`
// GeneratedJSONAsString is the configuration information to update the
// project YAML for generate.tasks. This is only used to store the
// configuration if GeneratedJSONStorageMethod is unset or is explicitly set
// to "db".
GeneratedJSONAsString GeneratedJSONFiles `bson:"generated_json,omitempty" json:"generated_json,omitempty"`
// GeneratedJSONStorageMethod describes how the generated JSON for
// generate.tasks is stored for this task before it's merged with the
// existing project YAML.
GeneratedJSONStorageMethod evergreen.ParserProjectStorageMethod `bson:"generated_json_storage_method,omitempty" json:"generated_json_storage_method,omitempty"`
// GenerateTasksError any encountered while generating tasks.
GenerateTasksError string `bson:"generate_error,omitempty" json:"generate_error,omitempty"`
// GeneratedTasksToActivate is only populated if we want to override activation for these generated tasks, because of stepback.
// Maps the build variant to a list of task names.
GeneratedTasksToActivate map[string][]string `bson:"generated_tasks_to_stepback,omitempty" json:"generated_tasks_to_stepback,omitempty"`
// NumGeneratedTasks is the number of tasks that this task has generated.
NumGeneratedTasks int `bson:"num_generated_tasks,omitempty" json:"num_generated_tasks,omitempty"`
// NumActivatedGeneratedTasks is the number of tasks that this task has generated and activated.
NumActivatedGeneratedTasks int `bson:"num_activated_generated_tasks,omitempty" json:"num_activated_generated_tasks,omitempty"`
// Fields set if triggered by an upstream build
TriggerID string `bson:"trigger_id,omitempty" json:"trigger_id,omitempty"`
TriggerType string `bson:"trigger_type,omitempty" json:"trigger_type,omitempty"`
TriggerEvent string `bson:"trigger_event,omitempty" json:"trigger_event,omitempty"`
CommitQueueMerge bool `bson:"commit_queue_merge,omitempty" json:"commit_queue_merge,omitempty"`
CanSync bool `bson:"can_sync" json:"can_sync"`
SyncAtEndOpts SyncAtEndOptions `bson:"sync_at_end_opts,omitempty" json:"sync_at_end_opts,omitempty"`
// IsEssentialToSucceed indicates that this task must finish in order for
// its build and version to be considered successful. For example, tasks
// selected by the GitHub PR alias must succeed for the GitHub PR requester
// before its build or version can be reported as successful, but tasks
// manually scheduled by the user afterwards are not required.
IsEssentialToSucceed bool `bson:"is_essential_to_succeed" json:"is_essential_to_succeed"`
// HasAnnotations indicates whether there exist task annotations with this task's
// execution and id that have a populated Issues key
HasAnnotations bool `bson:"has_annotations" json:"has_annotations"`
// NumNextTaskDispatches is the number of times the task has been dispatched to run on a
// host or in a container. This is used to determine if the task seems to be stuck.
NumNextTaskDispatches int `bson:"num_next_task_dispatches" json:"num_next_task_dispatches"`
}
// GeneratedJSONFiles represent files used by a task for generate.tasks to update the project YAML.
type GeneratedJSONFiles []string
// StepbackInfo helps determine which task to bisect to when performing stepback.
type StepbackInfo struct {
// LastFailingStepbackTaskId stores the last failing task while doing stepback.
LastFailingStepbackTaskId string `bson:"last_failing_stepback_task_id,omitempty" json:"last_failing_stepback_task_id"`
// LastPassingStepbackTaskId stores the last passing task while doing stepback.
LastPassingStepbackTaskId string `bson:"last_passing_stepback_task_id,omitempty" json:"last_passing_stepback_task_id"`
// NextStepbackTaskId stores the next task id to stepback to when doing bisect stepback. This
// is the middle of LastFailingStepbackTaskId and LastPassingStepbackTaskId of the last iteration.
NextStepbackTaskId string `bson:"next_stepback_task_id,omitempty" json:"next_stepback_task_id"`
// PreviousStepbackTaskId stores the last stepback iteration id.
PreviousStepbackTaskId string `bson:"previous_stepback_task_id,omitempty" json:"previous_stepback_task_id"`
// GeneratedStepbackInfo stores information on a generator for it's generated tasks.
GeneratedStepbackInfo []StepbackInfo `bson:"generated_stepback_info,omitempty" json:"generated_stepback_info,omitempty"`
// Generator fields only (responsible for propogating stepback in its generated tasks).
// DisplayName is the display name of the generated task.
DisplayName string `bson:"display_name,omitempty" json:"display_name,omitempty"`
// BuildVariant is the build variant of the generated task.
BuildVariant string `bson:"build_variant,omitempty" json:"build_variant,omitempty"`
}
// IsZero returns true if the StepbackInfo is empty or nil.
// It does not include GeneratedStepbackInfo in the check because
// those do not cause a generator to stepback.
func (s *StepbackInfo) IsZero() bool {
if s == nil {
return true
}
if s.LastFailingStepbackTaskId != "" && s.LastPassingStepbackTaskId != "" {
return false
}
// If the other fields are set but not the ones above, the struct should be considered empty.
return true
}
// GetStepbackInfoForGeneratedTask returns the StepbackInfo for a generated task that's
// on a generator task.
func (s *StepbackInfo) GetStepbackInfoForGeneratedTask(displayName string, buildVariant string) *StepbackInfo {
if s == nil {
return nil
}
for _, info := range s.GeneratedStepbackInfo {
if info.DisplayName == displayName && info.BuildVariant == buildVariant {
return &info
}
}
return nil
}
// ExecutionPlatform indicates the type of environment that the task runs in.
type ExecutionPlatform string
const (
// ExecutionPlatformHost indicates that the task runs in a host.
ExecutionPlatformHost ExecutionPlatform = "host"
// ExecutionPlatformContainer indicates that the task runs in a container.
ExecutionPlatformContainer ExecutionPlatform = "container"
)
// ContainerOptions represent options to create the container to run a task.
type ContainerOptions struct {
CPU int `bson:"cpu,omitempty" json:"cpu"`
MemoryMB int `bson:"memory_mb,omitempty" json:"memory_mb"`
WorkingDir string `bson:"working_dir,omitempty" json:"working_dir"`
Image string `bson:"image,omitempty" json:"image"`
// RepoCredsName is the name of the project container secret containing the
// repository credentials.
RepoCredsName string `bson:"repo_creds_name,omitempty" json:"repo_creds_name"`
OS evergreen.ContainerOS `bson:"os,omitempty" json:"os"`
Arch evergreen.ContainerArch `bson:"arch,omitempty" json:"arch"`
WindowsVersion evergreen.WindowsVersion `bson:"windows_version,omitempty" json:"windows_version"`
}
// IsZero implements the bsoncodec.Zeroer interface for the sake of defining the
// zero value for BSON marshalling.
func (o ContainerOptions) IsZero() bool {
return o == ContainerOptions{}
}
func (t *Task) MarshalBSON() ([]byte, error) { return mgobson.Marshal(t) }
func (t *Task) UnmarshalBSON(in []byte) error { return mgobson.Unmarshal(in, t) }
func (t *Task) GetTaskGroupString() string {
return fmt.Sprintf("%s_%s_%s_%s", t.TaskGroup, t.BuildVariant, t.Project, t.Version)
}
// S3Path returns the path to a task's directory dump in S3.
func (t *Task) S3Path(bv, name string) string {
return strings.Join([]string{t.Project, t.Version, bv, name, "latest"}, "/")
}
type SyncAtEndOptions struct {
Enabled bool `bson:"enabled,omitempty" json:"enabled,omitempty"`
Statuses []string `bson:"statuses,omitempty" json:"statuses,omitempty"`
Timeout time.Duration `bson:"timeout,omitempty" json:"timeout,omitempty"`
}
// Dependency represents a task that must be completed before the owning
// task can be scheduled.
type Dependency struct {
TaskId string `bson:"_id" json:"id"`
Status string `bson:"status" json:"status"`
Unattainable bool `bson:"unattainable" json:"unattainable"`
// Finished indicates if the task's dependency has finished running or not.
Finished bool `bson:"finished" json:"finished"`
// OmitGeneratedTasks causes tasks that depend on a generator task to not depend on
// the generated tasks if this is set
OmitGeneratedTasks bool `bson:"omit_generated_tasks,omitempty" json:"omit_generated_tasks,omitempty"`
}
// BaseTaskInfo is a subset of task fields that should be returned for patch tasks.
// The bson keys must match those of the actual task document
type BaseTaskInfo struct {
Id string `bson:"_id" json:"id"`
Status string `bson:"status" json:"status"`
}
type HostCreateDetail struct {
HostId string `bson:"host_id" json:"host_id"`
Error string `bson:"error" json:"error"`
}
func (d *Dependency) UnmarshalBSON(in []byte) error {
return mgobson.Unmarshal(in, d)
}
// SetBSON allows us to use dependency representation of both
// just task Ids and of true Dependency structs.
//
// TODO eventually drop all of this switching
func (d *Dependency) SetBSON(raw mgobson.Raw) error {
// copy the Dependency type to remove this SetBSON method but preserve bson struct tags
type nakedDep Dependency
var depCopy nakedDep
if err := raw.Unmarshal(&depCopy); err == nil {
if depCopy.TaskId != "" {
*d = Dependency(depCopy)
return nil
}
}
// hack to support the legacy depends_on, since we can't just unmarshal a string
strBytes, _ := mgobson.Marshal(mgobson.RawD{{Name: "str", Value: raw}})
var strStruct struct {
String string `bson:"str"`
}
if err := mgobson.Unmarshal(strBytes, &strStruct); err == nil {
if strStruct.String != "" {
d.TaskId = strStruct.String
d.Status = evergreen.TaskSucceeded
return nil
}
}
return mgobson.SetZero
}
type DisplayTaskCache struct {
execToDisplay map[string]*Task
displayTasks []*Task
}
func (c *DisplayTaskCache) Get(t *Task) (*Task, error) {
if parent, exists := c.execToDisplay[t.Id]; exists {
return parent, nil
}
displayTask, err := t.GetDisplayTask()
if err != nil {
return nil, err
}
if displayTask == nil {
return nil, nil
}
for _, execTask := range displayTask.ExecutionTasks {
c.execToDisplay[execTask] = displayTask
}
c.displayTasks = append(c.displayTasks, displayTask)
return displayTask, nil
}
func (c *DisplayTaskCache) List() []*Task { return c.displayTasks }
func NewDisplayTaskCache() DisplayTaskCache {
return DisplayTaskCache{execToDisplay: map[string]*Task{}, displayTasks: []*Task{}}
}
type AbortInfo struct {
User string `bson:"user,omitempty" json:"user,omitempty"`
TaskID string `bson:"task_id,omitempty" json:"task_id,omitempty"`
NewVersion string `bson:"new_version,omitempty" json:"new_version,omitempty"`
PRClosed bool `bson:"pr_closed,omitempty" json:"pr_closed,omitempty"`
}
var (
AllStatuses = "*"
)
// IsAbortable returns true if the task can be aborted.
func (t *Task) IsAbortable() bool {
return t.Status == evergreen.TaskStarted ||
t.Status == evergreen.TaskDispatched
}
// IsFinished returns true if the task is no longer running
func (t *Task) IsFinished() bool {
return evergreen.IsFinishedTaskStatus(t.Status)
}
// IsDispatchable returns true if the task should make progress towards
// dispatching to run.
func (t *Task) IsDispatchable() bool {
return t.IsHostDispatchable() || t.ShouldAllocateContainer() || t.IsContainerDispatchable()
}
// IsHostDispatchable returns true if the task should run on a host and can be
// dispatched.
func (t *Task) IsHostDispatchable() bool {
return t.IsHostTask() && t.WillRun()
}
// IsHostTask returns true if it's a task that runs on hosts.
func (t *Task) IsHostTask() bool {
return (t.ExecutionPlatform == "" || t.ExecutionPlatform == ExecutionPlatformHost) && !t.DisplayOnly
}
// IsStuckTask returns true if the task has been dispatched over the system limit
func (t *Task) IsStuckTask() bool {
return t.NumNextTaskDispatches >= evergreen.MaxTaskDispatchAttempts
}
// IsContainerTask returns true if it's a task that runs on containers.
func (t *Task) IsContainerTask() bool {
return t.ExecutionPlatform == ExecutionPlatformContainer
}
// IsRestartFailedOnly returns true if the task should only restart failed tests.
func (t *Task) IsRestartFailedOnly() bool {
return t.ResetFailedWhenFinished && !t.ResetWhenFinished
}
// ShouldAllocateContainer indicates whether a task should be allocated a
// container or not.
func (t *Task) ShouldAllocateContainer() bool {
if t.ContainerAllocated {
return false
}
if t.RemainingContainerAllocationAttempts() == 0 {
return false
}
return t.isContainerScheduled()
}
// RemainingContainerAllocationAttempts returns the number of times this task
// execution is allowed to try allocating a container.
func (t *Task) RemainingContainerAllocationAttempts() int {
return maxContainerAllocationAttempts - t.ContainerAllocationAttempts
}
// IsContainerDispatchable returns true if the task should run in a container
// and can be dispatched.
func (t *Task) IsContainerDispatchable() bool {
if !t.ContainerAllocated {
return false
}
return t.isContainerScheduled()
}
// isContainerTaskScheduled returns whether the task is in a state where it
// should eventually dispatch to run on a container and is logically equivalent
// to ScheduledContainerTasksQuery. This encompasses two potential states:
// 1. A container is not yet allocated to the task but it's ready to be
// allocated one. Note that this is a subset of all container tasks that
// could eventually run (i.e. evergreen.TaskWillRun from
// (Task).GetDisplayStatus), because a container task is not scheduled until
// all of its dependencies have been met.
// 2. The container is allocated but the agent has not picked up the task yet.
func (t *Task) isContainerScheduled() bool {
if !t.IsContainerTask() {
return false
}
if t.Status != evergreen.TaskUndispatched {
return false
}
if !t.Activated {
return false
}
if t.Priority <= evergreen.DisabledTaskPriority {
return false
}
if !t.OverrideDependencies {
for _, dep := range t.DependsOn {
if dep.Unattainable {
return false
}
if !dep.Finished {
return false
}
}
}
return true
}
// SatisfiesDependency checks a task the receiver task depends on
// to see if its status satisfies a dependency. If the "Status" field is
// unset, default to checking that is succeeded.
func (t *Task) SatisfiesDependency(depTask *Task) bool {
for _, dep := range t.DependsOn {
if dep.TaskId == depTask.Id {
switch dep.Status {
case evergreen.TaskSucceeded, "":
return depTask.Status == evergreen.TaskSucceeded
case evergreen.TaskFailed:
return depTask.Status == evergreen.TaskFailed
case AllStatuses:
return depTask.Status == evergreen.TaskFailed || depTask.Status == evergreen.TaskSucceeded || depTask.Blocked()
}
}
}
return false
}
func (t *Task) IsPatchRequest() bool {
return utility.StringSliceContains(evergreen.PatchRequesters, t.Requester)
}
// IsUnfinishedSystemUnresponsive returns true only if this is an unfinished system unresponsive task (i.e. not on max execution)
func (t *Task) IsUnfinishedSystemUnresponsive() bool {
return t.isSystemUnresponsive() && t.Execution < evergreen.MaxTaskExecution
}
func (t *Task) isSystemUnresponsive() bool {
// this is a legacy case
if t.Status == evergreen.TaskSystemUnresponse {
return true
}
if t.Details.Type == evergreen.CommandTypeSystem && t.Details.TimedOut && t.Details.Description == evergreen.TaskDescriptionHeartbeat {
return true
}
return false
}
func (t *Task) SetOverrideDependencies(userID string) error {
t.OverrideDependencies = true
event.LogTaskDependenciesOverridden(t.Id, t.Execution, userID)
return UpdateOne(
bson.M{
IdKey: t.Id,
},
bson.M{
"$set": bson.M{
OverrideDependenciesKey: true,
},
},
)
}
func (t *Task) AddDependency(ctx context.Context, d Dependency) error {
// ensure the dependency doesn't already exist
for _, existingDependency := range t.DependsOn {
if d.TaskId == t.Id {
grip.Error(message.Fields{
"message": "task is attempting to add a dependency on itself, skipping this dependency",
"task_id": t.Id,
"stack": string(debug.Stack()),
})
return nil
}
if existingDependency.TaskId == d.TaskId && existingDependency.Status == d.Status {
if existingDependency.Unattainable == d.Unattainable {
return nil // nothing to be done
}
return errors.Wrapf(t.MarkUnattainableDependency(ctx, existingDependency.TaskId, d.Unattainable),
"updating matching dependency '%s' for task '%s'", existingDependency.TaskId, t.Id)
}
}
t.DependsOn = append(t.DependsOn, d)
return UpdateOne(
bson.M{
IdKey: t.Id,
},
bson.M{
"$push": bson.M{
DependsOnKey: d,
},
},
)
}
func (t *Task) RemoveDependency(dependencyId string) error {
found := false
for i := len(t.DependsOn) - 1; i >= 0; i-- {
d := t.DependsOn[i]
if d.TaskId == dependencyId {
var dependsOn []Dependency
dependsOn = append(dependsOn, t.DependsOn[:i]...)
dependsOn = append(dependsOn, t.DependsOn[i+1:]...)
t.DependsOn = dependsOn
found = true
break
}
}
if !found {
return errors.Errorf("dependency '%s' not found", dependencyId)
}
query := bson.M{IdKey: t.Id}
update := bson.M{
"$pull": bson.M{
DependsOnKey: bson.M{
DependencyTaskIdKey: dependencyId,
},
},
}
return db.Update(Collection, query, update)
}
// DependenciesMet checks whether the dependencies for the task have all completed successfully.
// If any of the dependencies exist in the map that is passed in, they are
// used to check rather than fetching from the database. All queries
// are cached back into the map for later use.
func (t *Task) DependenciesMet(depCaches map[string]Task) (bool, error) {
if len(t.DependsOn) == 0 || t.OverrideDependencies || !utility.IsZeroTime(t.DependenciesMetTime) {
return true, nil
}
_, err := t.populateDependencyTaskCache(depCaches)
if err != nil {
return false, errors.WithStack(err)
}
for _, dependency := range t.DependsOn {
depTask, err := populateDependencyTaskCacheSingular(depCaches, dependency.TaskId)
if err != nil {
return false, err
}
if !t.SatisfiesDependency(depTask) {
return false, nil
}
}
// this is not exact, but depTask.FinishTime is not always set in time to use that
t.DependenciesMetTime = time.Now()
err = UpdateOne(
bson.M{IdKey: t.Id},
bson.M{
"$set": bson.M{DependenciesMetTimeKey: t.DependenciesMetTime},
})
grip.Error(message.WrapError(err, message.Fields{
"message": "task.DependenciesMet() failed to update task",
"task_id": t.Id}))
return true, nil
}
// populateDependencyTaskCache ensures that all the dependencies for the task are in the cache.
func (t *Task) populateDependencyTaskCache(depCache map[string]Task) ([]Task, error) {
var deps []Task
depIdsToQueryFor := make([]string, 0, len(t.DependsOn))
for _, dep := range t.DependsOn {
if cachedDep, ok := depCache[dep.TaskId]; !ok {
depIdsToQueryFor = append(depIdsToQueryFor, dep.TaskId)
} else {
deps = append(deps, cachedDep)
}
}
if len(depIdsToQueryFor) > 0 {
newDeps, err := FindWithFields(ByIds(depIdsToQueryFor), StatusKey, DependsOnKey, ActivatedKey)
if err != nil {
return nil, errors.WithStack(err)
}
// add queried dependencies to the cache
for _, newDep := range newDeps {
deps = append(deps, newDep)
depCache[newDep.Id] = newDep
}
}
return deps, nil
}
// GetFinishedBlockingDependencies gets all blocking tasks that are finished or blocked.
func (t *Task) GetFinishedBlockingDependencies(depCache map[string]Task) ([]Task, error) {
if len(t.DependsOn) == 0 || t.OverrideDependencies {
return nil, nil
}
// do this early to avoid caching tasks we won't need.
for _, dep := range t.DependsOn {
if dep.Unattainable {
return nil, nil
}
}
_, err := t.populateDependencyTaskCache(depCache)
if err != nil {
return nil, errors.WithStack(err)
}
blockedDeps := []Task{}
for _, dep := range t.DependsOn {
depTask, ok := depCache[dep.TaskId]
if !ok {
return nil, errors.Errorf("task '%s' is not in the cache", dep.TaskId)
}
if t.SatisfiesDependency(&depTask) {
continue
}
// If it is finished and did not statisfy the dependency, it is blocked.
if depTask.IsFinished() || depTask.Blocked() {
blockedDeps = append(blockedDeps, depTask)
}
}
return blockedDeps, nil
}
// GetDeactivatedBlockingDependencies gets all blocking tasks that are not finished and are not activated.
// These tasks are not going to run unless they are manually activated.
func (t *Task) GetDeactivatedBlockingDependencies(depCache map[string]Task) ([]string, error) {
_, err := t.populateDependencyTaskCache(depCache)
if err != nil {
return nil, errors.WithStack(err)
}
blockingDeps := []string{}
for _, dep := range t.DependsOn {
depTask, err := populateDependencyTaskCacheSingular(depCache, dep.TaskId)
if err != nil {
return nil, err
}
if !depTask.IsFinished() && !depTask.Activated {
blockingDeps = append(blockingDeps, depTask.Id)
}
}
return blockingDeps, nil
}
// populateDependencyTaskCacheSingular ensures that a single dependency for the task is in the cache.
// And if it is not, it queries the database for it.
func populateDependencyTaskCacheSingular(depCache map[string]Task, depId string) (*Task, error) {
if depTask, ok := depCache[depId]; ok {
return &depTask, nil
}
foundTask, err := FindOneId(depId)
if err != nil {
return nil, errors.Wrap(err, "finding dependency")
}
if foundTask == nil {
return nil, errors.Errorf("dependency '%s' not found", depId)
}
depCache[foundTask.Id] = *foundTask
return foundTask, nil
}
// AllDependenciesSatisfied inspects the tasks first-order
// dependencies with regards to the cached tasks, and reports if all
// of the dependencies have been satisfied.
//
// If the cached tasks do not include a dependency specified by one of
// the tasks, the function returns an error.
func (t *Task) AllDependenciesSatisfied(cache map[string]Task) (bool, error) {
if len(t.DependsOn) == 0 {
return true, nil
}
catcher := grip.NewBasicCatcher()
deps := []Task{}
for _, dep := range t.DependsOn {
cachedDep, err := populateDependencyTaskCacheSingular(cache, dep.TaskId)
if err != nil {
return false, err
}
deps = append(deps, *cachedDep)
}
if catcher.HasErrors() {
return false, catcher.Resolve()
}
for _, depTask := range deps {
if !t.SatisfiesDependency(&depTask) {
return false, nil
}
}
return true, nil
}
// MarkDependenciesFinished updates all direct dependencies on this task to
// cache whether or not this task has finished running.
func (t *Task) MarkDependenciesFinished(ctx context.Context, finished bool) error {
if t.DisplayOnly {
// This update can be skipped for display tasks since tasks are not
// allowed to have dependencies on display tasks.
return nil
}
_, err := evergreen.GetEnvironment().DB().Collection(Collection).UpdateMany(ctx,
bson.M{
DependsOnKey: bson.M{"$elemMatch": bson.M{
DependencyTaskIdKey: t.Id,
}},
},
bson.M{
"$set": bson.M{bsonutil.GetDottedKeyName(DependsOnKey, "$[elem]", DependencyFinishedKey): finished},
},
options.Update().SetArrayFilters(options.ArrayFilters{Filters: []interface{}{
bson.M{bsonutil.GetDottedKeyName("elem", DependencyTaskIdKey): t.Id},
}}),
)
if err != nil {
return errors.Wrap(err, "marking finished dependencies")
}
return nil
}
// FindTaskOnBaseCommit returns the task that is on the base commit.
func (t *Task) FindTaskOnBaseCommit() (*Task, error) {
return FindOne(db.Query(ByCommit(t.Revision, t.BuildVariant, t.DisplayName, t.Project, evergreen.RepotrackerVersionRequester)))
}
func (t *Task) FindTaskOnPreviousCommit() (*Task, error) {
return FindOne(db.Query(ByPreviousCommit(t.BuildVariant, t.DisplayName, t.Project, evergreen.RepotrackerVersionRequester, t.RevisionOrderNumber)).Sort([]string{"-" + RevisionOrderNumberKey}))
}
// CountSimilarFailingTasks returns a count of all tasks with the same project,
// same display name, and in other buildvariants, that have failed in the same
// revision
func (t *Task) CountSimilarFailingTasks() (int, error) {
return Count(db.Query(ByDifferentFailedBuildVariants(t.Revision, t.BuildVariant, t.DisplayName,
t.Project, t.Requester)))
}
// Find the previously completed task for the same project +
// build variant + display name combination as the specified task
func (t *Task) PreviousCompletedTask(project string, statuses []string) (*Task, error) {
if len(statuses) == 0 {
statuses = evergreen.TaskCompletedStatuses
}
query := db.Query(ByBeforeRevisionWithStatusesAndRequesters(t.RevisionOrderNumber, statuses, t.BuildVariant,
t.DisplayName, project, evergreen.SystemVersionRequesterTypes)).Sort([]string{"-" + RevisionOrderNumberKey})
return FindOne(query)
}
func (t *Task) cacheExpectedDuration() error {
return UpdateOne(
bson.M{
IdKey: t.Id,
},
bson.M{
"$set": bson.M{
DurationPredictionKey: t.DurationPrediction,
ExpectedDurationKey: t.DurationPrediction.Value,
ExpectedDurationStddevKey: t.DurationPrediction.StdDev,
},
},
)
}
// MarkAsContainerDispatched marks that the container task has been dispatched
// to a pod.
func (t *Task) MarkAsContainerDispatched(ctx context.Context, env evergreen.Environment, podID, agentVersion string) error {
dispatchedAt := time.Now()
query := ScheduledContainerTasksQuery()
query[IdKey] = t.Id
query[StatusKey] = evergreen.TaskUndispatched
query[ContainerAllocatedKey] = true
set := bson.M{
StatusKey: evergreen.TaskDispatched,
DispatchTimeKey: dispatchedAt,
LastHeartbeatKey: dispatchedAt,
PodIDKey: podID,