blob: 1c71f2f94c94b5a93dd3054b2e6f3c2ef14eaf65 [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package aggrmetrics
import (
"fmt"
"math/rand"
"strconv"
"testing"
"time"
"go.chromium.org/luci/common/tsmon/distribution"
"go.chromium.org/luci/gae/service/datastore"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/configs/prjcfg"
"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
"go.chromium.org/luci/cv/internal/cvtesting"
"go.chromium.org/luci/cv/internal/metrics"
"go.chromium.org/luci/cv/internal/run"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestRunAggregator(t *testing.T) {
t.Parallel()
Convey("runAggregator works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
// Truncate current time to seconds to deal with integer delays.
ct.Clock.Set(ct.Clock.Now().UTC().Add(time.Second).Truncate(time.Second))
pendingRunCountSent := func(project, configGroup string, mode run.Mode) interface{} {
return ct.TSMonSentValue(ctx, metrics.Public.PendingRunCount, project, configGroup, string(mode))
}
pendingRunDurationSent := func(project, configGroup string, mode run.Mode) *distribution.Distribution {
return ct.TSMonSentDistr(ctx, metrics.Public.PendingRunDuration, project, configGroup, string(mode))
}
maxPendingRunAgeSent := func(project, configGroup string, mode run.Mode) interface{} {
return ct.TSMonSentValue(ctx, metrics.Public.MaxPendingRunAge, project, configGroup, string(mode))
}
activeRunCountSent := func(project, configGroup string, mode run.Mode) interface{} {
return ct.TSMonSentValue(ctx, metrics.Public.ActiveRunCount, project, configGroup, string(mode))
}
activeRunDurationSent := func(project, configGroup string, mode run.Mode) *distribution.Distribution {
return ct.TSMonSentDistr(ctx, metrics.Public.ActiveRunDuration, project, configGroup, string(mode))
}
const lProject = "test_proj"
const configGroupName = "foo"
seededRand := rand.New(rand.NewSource(ct.Clock.Now().Unix()))
prjcfgtest.Create(ctx, lProject, &cfgpb.Config{
ConfigGroups: []*cfgpb.ConfigGroup{
{
Name: configGroupName,
},
},
})
putRun := func(project, configGroup string, mode run.Mode, s run.Status, ct, st time.Time) {
err := datastore.Put(ctx, &run.Run{
ID: common.MakeRunID(project, ct, 1, []byte(strconv.Itoa(seededRand.Int()))),
ConfigGroupID: prjcfg.MakeConfigGroupID("deedbeef", configGroup),
Mode: mode,
CreateTime: ct,
StartTime: st,
Status: s,
})
if err != nil {
panic(err)
}
}
mustReport := func(active ...string) {
ra := runsAggregator{}
err := ra.report(ctx, active)
So(err, ShouldBeNil)
}
Convey("Skip reporting for disabled project", func() {
prjcfgtest.Disable(ctx, lProject)
mustReport(lProject)
So(ct.TSMonStore.GetAll(ctx), ShouldBeEmpty)
})
Convey("Enabled projects get zero data reported when no interested Runs", func() {
mustReport(lProject)
So(pendingRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 0)
So(pendingRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 0)
So(pendingRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 0)
So(pendingRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 0)
So(maxPendingRunAgeSent(lProject, configGroupName, run.DryRun), ShouldEqual, 0)
So(maxPendingRunAgeSent(lProject, configGroupName, run.FullRun), ShouldEqual, 0)
So(activeRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 0)
So(activeRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 0)
So(activeRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 0)
So(activeRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 0)
So(ct.TSMonStore.GetAll(ctx), ShouldHaveLength, 10)
})
Convey("Report pending Runs", func() {
now := ct.Clock.Now()
// Dry Run
putRun(lProject, configGroupName, run.DryRun, run.Status_PENDING, now.Add(-time.Second), time.Time{})
putRun(lProject, configGroupName, run.DryRun, run.Status_PENDING, now.Add(-time.Minute), time.Time{})
putRun(lProject, configGroupName, run.DryRun, run.Status_RUNNING, now.Add(-time.Hour), now.Add(-time.Hour)) // active run won't be reported
// Quick DryRun
putRun(lProject, configGroupName, run.QuickDryRun, run.Status_PENDING, now.Add(-2*time.Second), time.Time{})
// Full Run
putRun(lProject, configGroupName, run.FullRun, run.Status_PENDING, now.Add(-time.Minute), time.Time{})
putRun(lProject, configGroupName, run.FullRun, run.Status_PENDING, now.Add(-time.Hour), time.Time{})
putRun(lProject, configGroupName, run.FullRun, run.Status_SUCCEEDED, now.Add(-time.Hour), now.Add(-time.Hour)) // ended run won't be reported
mustReport(lProject)
So(pendingRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 2)
So(pendingRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 2)
So(pendingRunDurationSent(lProject, configGroupName, run.DryRun).Sum(), ShouldEqual, float64((time.Second + time.Minute).Milliseconds()))
So(maxPendingRunAgeSent(lProject, configGroupName, run.DryRun), ShouldEqual, time.Minute.Milliseconds())
So(pendingRunCountSent(lProject, configGroupName, run.QuickDryRun), ShouldEqual, 1)
So(pendingRunDurationSent(lProject, configGroupName, run.QuickDryRun).Count(), ShouldEqual, 1)
So(pendingRunDurationSent(lProject, configGroupName, run.QuickDryRun).Sum(), ShouldEqual, float64((2 * time.Second).Milliseconds()))
So(maxPendingRunAgeSent(lProject, configGroupName, run.QuickDryRun), ShouldEqual, (2 * time.Second).Milliseconds())
So(pendingRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 2)
So(pendingRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 2)
So(pendingRunDurationSent(lProject, configGroupName, run.FullRun).Sum(), ShouldEqual, float64((time.Hour + time.Minute).Milliseconds()))
So(maxPendingRunAgeSent(lProject, configGroupName, run.FullRun), ShouldEqual, time.Hour.Milliseconds())
})
Convey("Report active Runs", func() {
now := ct.Clock.Now()
// Dry Run
putRun(lProject, configGroupName, run.DryRun, run.Status_PENDING, now.Add(-time.Second), time.Time{}) // pending runs won't be reported
putRun(lProject, configGroupName, run.DryRun, run.Status_RUNNING, now.Add(-time.Minute), now.Add(-time.Minute))
putRun(lProject, configGroupName, run.DryRun, run.Status_SUCCEEDED, now.Add(-time.Hour), now.Add(-time.Hour)) // ended run won't be reported
// Quick DryRun
putRun(lProject, configGroupName, run.QuickDryRun, run.Status_RUNNING, now.Add(-2*time.Second), now.Add(-time.Second))
// Full Run
putRun(lProject, configGroupName, run.FullRun, run.Status_WAITING_FOR_SUBMISSION, now.Add(-time.Minute), now.Add(-time.Minute))
putRun(lProject, configGroupName, run.FullRun, run.Status_SUBMITTING, now.Add(-time.Hour), now.Add(-time.Hour))
putRun(lProject, configGroupName, run.FullRun, run.Status_CANCELLED, now.Add(-time.Hour), now.Add(-time.Hour)) // ended run won't be reported
mustReport(lProject)
So(activeRunCountSent(lProject, configGroupName, run.DryRun), ShouldEqual, 1)
So(activeRunDurationSent(lProject, configGroupName, run.DryRun).Count(), ShouldEqual, 1)
So(activeRunDurationSent(lProject, configGroupName, run.DryRun).Sum(), ShouldEqual, (time.Minute).Seconds())
So(activeRunCountSent(lProject, configGroupName, run.QuickDryRun), ShouldEqual, 1)
So(activeRunDurationSent(lProject, configGroupName, run.QuickDryRun).Count(), ShouldEqual, 1)
So(activeRunDurationSent(lProject, configGroupName, run.QuickDryRun).Sum(), ShouldEqual, (time.Second).Seconds())
So(activeRunCountSent(lProject, configGroupName, run.FullRun), ShouldEqual, 2)
So(activeRunDurationSent(lProject, configGroupName, run.FullRun).Count(), ShouldEqual, 2)
So(activeRunDurationSent(lProject, configGroupName, run.FullRun).Sum(), ShouldEqual, (time.Hour + time.Minute).Seconds())
})
putManyRuns := func(n, numProject, numConfigGroup int) {
for projectID := 0; projectID < numProject; projectID++ {
cfg := &cfgpb.Config{
ConfigGroups: make([]*cfgpb.ConfigGroup, numConfigGroup),
}
for cgID := 0; cgID < numConfigGroup; cgID++ {
cfg.ConfigGroups[cgID] = &cfgpb.ConfigGroup{
Name: fmt.Sprintf("cg-%03d", cgID),
}
}
prjcfgtest.Create(ctx, fmt.Sprintf("p-%03d", projectID), cfg)
}
for i := 0; i < n; i++ {
project := fmt.Sprintf("p-%03d", i%numProject)
configGroup := fmt.Sprintf("cg-%03d", (i/numProject)%numConfigGroup)
created := ct.Clock.Now().Add(-time.Duration(i) * time.Second)
started := ct.Clock.Now().Add(-time.Duration(i) * time.Second)
putRun(project, configGroup, run.DryRun, run.Status_RUNNING, created, started)
}
}
Convey("Can handle a lot of Runs and projects", func() {
numProject := 16
numConfigGroup := 8
So(numProject*numConfigGroup, ShouldBeLessThan, maxRuns)
putManyRuns(maxRuns, numProject, numConfigGroup)
mustReport()
reportedCnt := 0
for projectID := 0; projectID < numProject; projectID++ {
for cgID := 0; cgID < numConfigGroup; cgID++ {
project := fmt.Sprintf("p-%03d", projectID)
configGroup := fmt.Sprintf("cg-%03d", cgID)
So(activeRunCountSent(project, configGroup, run.DryRun), ShouldBeGreaterThan, 0)
reportedCnt += int(activeRunCountSent(project, configGroup, run.DryRun).(int64))
So(activeRunDurationSent(project, configGroup, run.DryRun).Sum(), ShouldBeGreaterThan, 0)
So(activeRunDurationSent(project, configGroup, run.DryRun).Count(), ShouldBeGreaterThan, 0)
}
}
So(reportedCnt, ShouldEqual, maxRuns)
})
Convey("Refuses to report anything if there are too many active Runs", func() {
putManyRuns(maxRuns+1, 16, 8)
ra := runsAggregator{}
So(ra.report(ctx, []string{lProject}), ShouldErrLike, "too many active Runs")
So(ct.TSMonStore.GetAll(ctx), ShouldBeEmpty)
})
})
}