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 }