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  }