go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/metrics/builder_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 metrics
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  
    24  	"go.chromium.org/luci/common/clock/testclock"
    25  	"go.chromium.org/luci/common/tsmon"
    26  	"go.chromium.org/luci/common/tsmon/target"
    27  
    28  	"go.chromium.org/luci/gae/impl/memory"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  
    31  	"go.chromium.org/luci/buildbucket/appengine/model"
    32  	pb "go.chromium.org/luci/buildbucket/proto"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  )
    36  
    37  func TestReportBuilderMetrics(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	Convey("ReportBuilderMetrics", t, func() {
    41  		ctx, clock := testclock.UseTime(
    42  			WithServiceInfo(memory.Use(context.Background()), "svc", "job", "ins"),
    43  			testclock.TestTimeUTC.Truncate(time.Millisecond),
    44  		)
    45  		ctx, _ = tsmon.WithDummyInMemory(ctx)
    46  		datastore.GetTestable(ctx).AutoIndex(true)
    47  		datastore.GetTestable(ctx).Consistent(true)
    48  
    49  		store := tsmon.Store(ctx)
    50  		prj, bkt := "infra", "ci"
    51  		task := &target.Task{
    52  			ServiceName: "svc",
    53  			JobName:     "job",
    54  			HostName:    "ins",
    55  			TaskNum:     0,
    56  		}
    57  		store.SetDefaultTarget(task)
    58  		target := func(builder string) context.Context {
    59  			return WithBuilder(ctx, prj, bkt, builder)
    60  		}
    61  
    62  		createBuilder := func(builder string) error {
    63  			return datastore.Put(
    64  				ctx,
    65  				&model.Bucket{Parent: model.ProjectKey(ctx, prj), ID: bkt},
    66  				&model.Builder{Parent: model.BucketKey(ctx, prj, bkt), ID: builder},
    67  				&model.BuilderStat{ID: prj + ":" + bkt + ":" + builder},
    68  			)
    69  		}
    70  		deleteBuilder := func(builder string) error {
    71  			return datastore.Delete(
    72  				ctx,
    73  				&model.Bucket{Parent: model.ProjectKey(ctx, prj), ID: bkt},
    74  				&model.Builder{Parent: model.BucketKey(ctx, prj, bkt), ID: builder},
    75  				&model.BuilderStat{ID: prj + ":" + bkt + ":" + builder},
    76  			)
    77  		}
    78  
    79  		Convey("report v2.BuilderPresence", func() {
    80  			So(createBuilder("b1"), ShouldBeNil)
    81  			So(createBuilder("b2"), ShouldBeNil)
    82  			So(ReportBuilderMetrics(ctx), ShouldBeNil)
    83  			So(store.Get(target("b1"), V2.BuilderPresence, time.Time{}, nil), ShouldEqual, true)
    84  			So(store.Get(target("b2"), V2.BuilderPresence, time.Time{}, nil), ShouldEqual, true)
    85  			So(store.Get(target("b3"), V2.BuilderPresence, time.Time{}, nil), ShouldBeNil)
    86  
    87  			Convey("w/o removed builder", func() {
    88  				So(deleteBuilder("b1"), ShouldBeNil)
    89  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
    90  				So(store.Get(target("b1"), V2.BuilderPresence, time.Time{}, nil), ShouldBeNil)
    91  				So(store.Get(target("b2"), V2.BuilderPresence, time.Time{}, nil), ShouldEqual, true)
    92  			})
    93  
    94  			Convey("w/o inactive builder", func() {
    95  				// Let's pretend that b1 was inactive for 4 weeks, and
    96  				// got unregistered from the BuilderStat.
    97  				So(datastore.Delete(ctx, &model.BuilderStat{ID: prj + ":" + bkt + ":b1"}), ShouldBeNil)
    98  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
    99  				// b1 should no longer be reported in the presence metric.
   100  				So(store.Get(target("b1"), V2.BuilderPresence, time.Time{}, nil), ShouldBeNil)
   101  				So(store.Get(target("b2"), V2.BuilderPresence, time.Time{}, nil), ShouldEqual, true)
   102  			})
   103  		})
   104  
   105  		Convey("report MaxAgeScheduled", func() {
   106  			So(createBuilder("b1"), ShouldBeNil)
   107  			builderID := &pb.BuilderID{
   108  				Project: prj,
   109  				Bucket:  bkt,
   110  				Builder: "b1",
   111  			}
   112  			builds := []*model.Build{
   113  				{
   114  					Proto: &pb.Build{Id: 1, Builder: builderID, Status: pb.Status_SCHEDULED},
   115  					Tags:  []string{"builder:b1"},
   116  				},
   117  				{
   118  					Proto: &pb.Build{Id: 2, Builder: builderID, Status: pb.Status_SCHEDULED},
   119  					Tags:  []string{"builder:b1"},
   120  				},
   121  			}
   122  			builds[0].NeverLeased = true
   123  			builds[1].NeverLeased = false
   124  			now := clock.Now()
   125  
   126  			Convey("never_leased_age >= leased_age", func() {
   127  				builds[0].Proto.CreateTime = timestamppb.New(now.Add(-2 * time.Hour))
   128  				builds[1].Proto.CreateTime = timestamppb.New(now.Add(-1 * time.Hour))
   129  				So(datastore.Put(ctx, builds[0], builds[1]), ShouldBeNil)
   130  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   131  
   132  				age := (2 * time.Hour).Seconds()
   133  
   134  				Convey("v1", func() {
   135  					// the ages should be the same.
   136  					fields := []any{bkt, "b1", true}
   137  					So(store.Get(ctx, V1.MaxAgeScheduled, time.Time{}, fields), ShouldEqual, age)
   138  					fields = []any{bkt, "b1", false}
   139  					So(store.Get(ctx, V1.MaxAgeScheduled, time.Time{}, fields), ShouldEqual, age)
   140  				})
   141  
   142  				Convey("v2", func() {
   143  					So(store.Get(target("b1"), V2.MaxAgeScheduled, time.Time{}, nil), ShouldEqual, age)
   144  				})
   145  			})
   146  
   147  			Convey("v1: never_leased_age < leased_age", func() {
   148  				builds[0].Proto.CreateTime = timestamppb.New(now.Add(-1 * time.Hour))
   149  				builds[1].Proto.CreateTime = timestamppb.New(now.Add(-2 * time.Hour))
   150  				So(datastore.Put(ctx, builds[0], builds[1]), ShouldBeNil)
   151  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   152  
   153  				age := time.Hour.Seconds()
   154  
   155  				Convey("v1", func() {
   156  					// the ages should be different.
   157  					fields := []any{bkt, "b1", true}
   158  					So(store.Get(ctx, V1.MaxAgeScheduled, time.Time{}, fields), ShouldEqual, age)
   159  					fields = []any{bkt, "b1", false}
   160  					So(store.Get(ctx, V1.MaxAgeScheduled, time.Time{}, fields), ShouldEqual, 2*age)
   161  				})
   162  
   163  				Convey("v2", func() {
   164  					So(store.Get(target("b1"), V2.MaxAgeScheduled, time.Time{}, nil), ShouldEqual, 2*age)
   165  				})
   166  			})
   167  
   168  			Convey("w/ swarming config in bucket", func() {
   169  				So(datastore.Put(ctx, &model.Bucket{
   170  					Parent: model.ProjectKey(ctx, prj), ID: bkt,
   171  					Proto: &pb.Bucket{Swarming: &pb.Swarming{}},
   172  				}), ShouldBeNil)
   173  				So(datastore.Put(ctx, builds[0], builds[1]), ShouldBeNil)
   174  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   175  
   176  				Convey("v1", func() {
   177  					// Data should have been reported with "luci.$project.$bucket"
   178  					fields := []any{bkt, "b1", true}
   179  					So(store.Get(ctx, V1.MaxAgeScheduled, time.Time{}, fields), ShouldBeNil)
   180  					fields = []any{"luci." + prj + "." + bkt, "b1", true}
   181  					So(store.Get(ctx, V1.MaxAgeScheduled, time.Time{}, fields), ShouldNotBeNil)
   182  				})
   183  
   184  				Convey("v2", func() {
   185  					// V2 doesn't care. It always reports the bucket name as it is.
   186  					So(store.Get(target("b1"), V2.MaxAgeScheduled, time.Time{}, nil), ShouldNotBeNil)
   187  				})
   188  			})
   189  		})
   190  
   191  		Convey("report ConsecutiveFailures", func() {
   192  			So(createBuilder("b1"), ShouldBeNil)
   193  			builderID := &pb.BuilderID{
   194  				Project: prj,
   195  				Bucket:  bkt,
   196  				Builder: "b1",
   197  			}
   198  			B := func(status pb.Status, changedAt time.Time) *model.Build {
   199  				return &model.Build{
   200  					Proto: &pb.Build{
   201  						Builder: builderID, Status: status,
   202  						UpdateTime: timestamppb.New(changedAt)},
   203  					Tags: []string{"builder:b1"},
   204  				}
   205  			}
   206  			count := func(s string) any {
   207  				return store.Get(target("b1"), V2.ConsecutiveFailureCount, time.Time{}, []any{s})
   208  			}
   209  			t := clock.Now()
   210  
   211  			Convey("w/o success", func() {
   212  				builds := []*model.Build{
   213  					B(pb.Status_CANCELED, t.Add(-4*time.Minute)),
   214  					B(pb.Status_FAILURE, t.Add(-3*time.Minute)),
   215  					B(pb.Status_INFRA_FAILURE, t.Add(-2*time.Minute)),
   216  					B(pb.Status_CANCELED, t.Add(-1*time.Minute)),
   217  				}
   218  				So(datastore.Put(ctx, builds), ShouldBeNil)
   219  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   220  				So(count("FAILURE"), ShouldEqual, 1)
   221  				So(count("INFRA_FAILURE"), ShouldEqual, 1)
   222  				So(count("CANCELED"), ShouldEqual, 2)
   223  			})
   224  
   225  			Convey("w/ success only", func() {
   226  				builds := []*model.Build{
   227  					B(pb.Status_SUCCESS, t.Add(-3*time.Minute)),
   228  					B(pb.Status_SUCCESS, t.Add(-2*time.Minute)),
   229  					B(pb.Status_SUCCESS, t.Add(-1*time.Minute)),
   230  				}
   231  				So(datastore.Put(ctx, builds), ShouldBeNil)
   232  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   233  				// The count for each status should still be reported w/o 0.
   234  				So(count("FAILURE"), ShouldEqual, 0)
   235  				So(count("INFRA_FAILURE"), ShouldEqual, 0)
   236  				So(count("CANCELED"), ShouldEqual, 0)
   237  			})
   238  
   239  			Convey("w/ a series of failures after success", func() {
   240  				builds := []*model.Build{
   241  					B(pb.Status_CANCELED, t.Add(-6*time.Minute)),
   242  					B(pb.Status_SUCCESS, t.Add(-5*time.Minute)),
   243  					B(pb.Status_FAILURE, t.Add(-4*time.Minute)),
   244  					B(pb.Status_FAILURE, t.Add(-3*time.Minute)),
   245  					B(pb.Status_INFRA_FAILURE, t.Add(-2*time.Minute)),
   246  					B(pb.Status_CANCELED, t.Add(-1*time.Minute)),
   247  				}
   248  				So(datastore.Put(ctx, builds), ShouldBeNil)
   249  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   250  				// 2 failures, 1 infra-failure, and 1 cancel.
   251  				// Note that the first cancel is ignored because it happened before
   252  				// the success.
   253  				So(count("FAILURE"), ShouldEqual, 2)
   254  				So(count("INFRA_FAILURE"), ShouldEqual, 1)
   255  				So(count("CANCELED"), ShouldEqual, 1)
   256  			})
   257  			Convey("w/ a series of failures before a success", func() {
   258  				builds := []*model.Build{
   259  					B(pb.Status_CANCELED, t.Add(-5*time.Minute)),
   260  					B(pb.Status_SUCCESS, t.Add(-4*time.Minute)),
   261  					B(pb.Status_FAILURE, t.Add(-3*time.Minute)),
   262  					B(pb.Status_INFRA_FAILURE, t.Add(-2*time.Minute)),
   263  					B(pb.Status_CANCELED, t.Add(-1*time.Minute)),
   264  					B(pb.Status_SUCCESS, t.Add(time.Minute)),
   265  				}
   266  				So(datastore.Put(ctx, builds), ShouldBeNil)
   267  				So(ReportBuilderMetrics(ctx), ShouldBeNil)
   268  				So(count("FAILURE"), ShouldEqual, 0)
   269  				So(count("INFRA_FAILURE"), ShouldEqual, 0)
   270  				So(count("CANCELED"), ShouldEqual, 0)
   271  			})
   272  		})
   273  	})
   274  }