go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/aggrmetrics/runs_test.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package aggrmetrics 16 17 import ( 18 "fmt" 19 "math/rand" 20 "strconv" 21 "testing" 22 "time" 23 24 "go.chromium.org/luci/common/tsmon/distribution" 25 "go.chromium.org/luci/gae/service/datastore" 26 27 cfgpb "go.chromium.org/luci/cv/api/config/v2" 28 "go.chromium.org/luci/cv/internal/common" 29 "go.chromium.org/luci/cv/internal/configs/prjcfg" 30 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 31 "go.chromium.org/luci/cv/internal/cvtesting" 32 "go.chromium.org/luci/cv/internal/metrics" 33 "go.chromium.org/luci/cv/internal/run" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 func TestRunAggregator(t *testing.T) { 40 t.Parallel() 41 42 Convey("runAggregator works", t, func() { 43 ct := cvtesting.Test{} 44 ctx, cancel := ct.SetUp(t) 45 defer cancel() 46 // Truncate current time to seconds to deal with integer delays. 47 ct.Clock.Set(ct.Clock.Now().UTC().Add(time.Second).Truncate(time.Second)) 48 49 pendingRunCountSent := func(project, configGroup string, mode run.Mode) any { 50 return ct.TSMonSentValue(ctx, metrics.Public.PendingRunCount, project, configGroup, string(mode)) 51 } 52 pendingRunDurationSent := func(project, configGroup string, mode run.Mode) *distribution.Distribution { 53 return ct.TSMonSentDistr(ctx, metrics.Public.PendingRunDuration, project, configGroup, string(mode)) 54 } 55 maxPendingRunAgeSent := func(project, configGroup string, mode run.Mode) any { 56 return ct.TSMonSentValue(ctx, metrics.Public.MaxPendingRunAge, project, configGroup, string(mode)) 57 } 58 activeRunCountSent := func(project, configGroup string, mode run.Mode) any { 59 return ct.TSMonSentValue(ctx, metrics.Public.ActiveRunCount, project, configGroup, string(mode)) 60 } 61 activeRunDurationSent := func(project, configGroup string, mode run.Mode) *distribution.Distribution { 62 return ct.TSMonSentDistr(ctx, metrics.Public.ActiveRunDuration, project, configGroup, string(mode)) 63 } 64 65 const lProject = "test_proj" 66 const configGroupName = "foo" 67 seededRand := rand.New(rand.NewSource(ct.Clock.Now().Unix())) 68 69 prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ 70 ConfigGroups: []*cfgpb.ConfigGroup{ 71 { 72 Name: configGroupName, 73 }, 74 }, 75 }) 76 77 putRun := func(project, configGroup string, mode run.Mode, s run.Status, ct, st time.Time) { 78 err := datastore.Put(ctx, &run.Run{ 79 ID: common.MakeRunID(project, ct, 1, []byte(strconv.Itoa(seededRand.Int()))), 80 ConfigGroupID: prjcfg.MakeConfigGroupID("deedbeef", configGroup), 81 Mode: mode, 82 CreateTime: ct, 83 StartTime: st, 84 Status: s, 85 }) 86 if err != nil { 87 panic(err) 88 } 89 } 90 91 mustReport := func(active ...string) { 92 ra := runsAggregator{} 93 err := ra.report(ctx, active) 94 So(err, ShouldBeNil) 95 } 96 97 Convey("Skip reporting for disabled project", func() { 98 prjcfgtest.Disable(ctx, lProject) 99 mustReport(lProject) 100 So(ct.TSMonStore.GetAll(ctx), ShouldBeEmpty) 101 }) 102 103 Convey("Enabled projects get zero data reported when no interested Runs", func() { 104 mustReport(lProject) 105 So(pendingRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 0) 106 So(pendingRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 0) 107 So(pendingRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 0) 108 So(pendingRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 0) 109 So(maxPendingRunAgeSent(lProject, configGroupName, run.DryRun), ShouldEqual, 0) 110 So(maxPendingRunAgeSent(lProject, configGroupName, run.FullRun), ShouldEqual, 0) 111 So(activeRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 0) 112 So(activeRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 0) 113 So(activeRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 0) 114 So(activeRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 0) 115 116 So(ct.TSMonStore.GetAll(ctx), ShouldHaveLength, 10) 117 }) 118 119 Convey("Report pending Runs", func() { 120 now := ct.Clock.Now() 121 // Dry Run 122 putRun(lProject, configGroupName, run.DryRun, run.Status_PENDING, now.Add(-time.Second), time.Time{}) 123 putRun(lProject, configGroupName, run.DryRun, run.Status_PENDING, now.Add(-time.Minute), time.Time{}) 124 putRun(lProject, configGroupName, run.DryRun, run.Status_RUNNING, now.Add(-time.Hour), now.Add(-time.Hour)) // active run won't be reported 125 126 // New Patchset Run 127 putRun(lProject, configGroupName, run.NewPatchsetRun, run.Status_PENDING, now.Add(-2*time.Second), time.Time{}) 128 129 // Full Run 130 putRun(lProject, configGroupName, run.FullRun, run.Status_PENDING, now.Add(-time.Minute), time.Time{}) 131 putRun(lProject, configGroupName, run.FullRun, run.Status_PENDING, now.Add(-time.Hour), time.Time{}) 132 putRun(lProject, configGroupName, run.FullRun, run.Status_SUCCEEDED, now.Add(-time.Hour), now.Add(-time.Hour)) // ended run won't be reported 133 134 mustReport(lProject) 135 So(pendingRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 2) 136 So(pendingRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 2) 137 So(pendingRunDurationSent(lProject, configGroupName, run.DryRun).Sum(), ShouldEqual, float64((time.Second + time.Minute).Milliseconds())) 138 So(maxPendingRunAgeSent(lProject, configGroupName, run.DryRun), ShouldEqual, time.Minute.Milliseconds()) 139 140 So(pendingRunCountSent(lProject, configGroupName, run.NewPatchsetRun), ShouldEqual, 1) 141 So(pendingRunDurationSent(lProject, configGroupName, run.NewPatchsetRun).Count(), ShouldEqual, 1) 142 So(pendingRunDurationSent(lProject, configGroupName, run.NewPatchsetRun).Sum(), ShouldEqual, float64((2 * time.Second).Milliseconds())) 143 So(maxPendingRunAgeSent(lProject, configGroupName, run.NewPatchsetRun), ShouldEqual, (2 * time.Second).Milliseconds()) 144 145 So(pendingRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 2) 146 So(pendingRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 2) 147 So(pendingRunDurationSent(lProject, configGroupName, run.FullRun).Sum(), ShouldEqual, float64((time.Hour + time.Minute).Milliseconds())) 148 So(maxPendingRunAgeSent(lProject, configGroupName, run.FullRun), ShouldEqual, time.Hour.Milliseconds()) 149 }) 150 151 Convey("Report active Runs", func() { 152 now := ct.Clock.Now() 153 // Dry Run 154 putRun(lProject, configGroupName, run.DryRun, run.Status_PENDING, now.Add(-time.Second), time.Time{}) // pending runs won't be reported 155 putRun(lProject, configGroupName, run.DryRun, run.Status_RUNNING, now.Add(-time.Minute), now.Add(-time.Minute)) 156 putRun(lProject, configGroupName, run.DryRun, run.Status_SUCCEEDED, now.Add(-time.Hour), now.Add(-time.Hour)) // ended run won't be reported 157 158 // New Patchset Run 159 putRun(lProject, configGroupName, run.NewPatchsetRun, run.Status_RUNNING, now.Add(-2*time.Second), now.Add(-time.Second)) 160 161 // Full Run 162 putRun(lProject, configGroupName, run.FullRun, run.Status_WAITING_FOR_SUBMISSION, now.Add(-time.Minute), now.Add(-time.Minute)) 163 putRun(lProject, configGroupName, run.FullRun, run.Status_SUBMITTING, now.Add(-time.Hour), now.Add(-time.Hour)) 164 putRun(lProject, configGroupName, run.FullRun, run.Status_CANCELLED, now.Add(-time.Hour), now.Add(-time.Hour)) // ended run won't be reported 165 166 mustReport(lProject) 167 So(activeRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 1) 168 So(activeRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 1) 169 So(activeRunDurationSent(lProject, configGroupName, run.DryRun).Sum(), ShouldEqual, (time.Minute).Seconds()) 170 171 So(activeRunCountSent(lProject, configGroupName, run.NewPatchsetRun), ShouldEqual, 1) 172 So(activeRunDurationSent(lProject, configGroupName, run.NewPatchsetRun).Count(), ShouldEqual, 1) 173 So(activeRunDurationSent(lProject, configGroupName, run.NewPatchsetRun).Sum(), ShouldEqual, (time.Second).Seconds()) 174 175 So(activeRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 2) 176 So(activeRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 2) 177 So(activeRunDurationSent(lProject, configGroupName, run.FullRun).Sum(), ShouldEqual, (time.Hour + time.Minute).Seconds()) 178 }) 179 180 putManyRuns := func(n, numProject, numConfigGroup int) { 181 for projectID := 0; projectID < numProject; projectID++ { 182 cfg := &cfgpb.Config{ 183 ConfigGroups: make([]*cfgpb.ConfigGroup, numConfigGroup), 184 } 185 for cgID := 0; cgID < numConfigGroup; cgID++ { 186 cfg.ConfigGroups[cgID] = &cfgpb.ConfigGroup{ 187 Name: fmt.Sprintf("cg-%03d", cgID), 188 } 189 } 190 prjcfgtest.Create(ctx, fmt.Sprintf("p-%03d", projectID), cfg) 191 } 192 193 for i := 0; i < n; i++ { 194 project := fmt.Sprintf("p-%03d", i%numProject) 195 configGroup := fmt.Sprintf("cg-%03d", (i/numProject)%numConfigGroup) 196 created := ct.Clock.Now().Add(-time.Duration(i) * time.Second) 197 started := ct.Clock.Now().Add(-time.Duration(i) * time.Second) 198 putRun(project, configGroup, run.DryRun, run.Status_RUNNING, created, started) 199 } 200 } 201 202 Convey("Can handle a lot of Runs and projects", func() { 203 numProject := 16 204 numConfigGroup := 8 205 So(numProject*numConfigGroup, ShouldBeLessThan, maxRuns) 206 putManyRuns(maxRuns, numProject, numConfigGroup) 207 mustReport() 208 209 reportedCnt := 0 210 for projectID := 0; projectID < numProject; projectID++ { 211 for cgID := 0; cgID < numConfigGroup; cgID++ { 212 project := fmt.Sprintf("p-%03d", projectID) 213 configGroup := fmt.Sprintf("cg-%03d", cgID) 214 So(activeRunCountSent(project, configGroup, run.DryRun), ShouldBeGreaterThan, 0) 215 reportedCnt += int(activeRunCountSent(project, configGroup, run.DryRun).(int64)) 216 So(activeRunDurationSent(project, configGroup, run.DryRun).Sum(), ShouldBeGreaterThan, 0) 217 So(activeRunDurationSent(project, configGroup, run.DryRun).Count(), ShouldBeGreaterThan, 0) 218 } 219 } 220 So(reportedCnt, ShouldEqual, maxRuns) 221 }) 222 223 Convey("Refuses to report anything if there are too many active Runs", func() { 224 putManyRuns(maxRuns+1, 16, 8) 225 ra := runsAggregator{} 226 So(ra.report(ctx, []string{lProject}), ShouldErrLike, "too many active Runs") 227 So(ct.TSMonStore.GetAll(ctx), ShouldBeEmpty) 228 }) 229 }) 230 }