go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/buildsource/buildbucket/build_sync_test.go (about)

     1  // Copyright 2017 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 buildbucket
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/base64"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/alicebob/miniredis/v2"
    30  	"github.com/golang/mock/gomock"
    31  	"github.com/gomodule/redigo/redis"
    32  	"google.golang.org/protobuf/encoding/protojson"
    33  	"google.golang.org/protobuf/proto"
    34  	"google.golang.org/protobuf/types/known/structpb"
    35  	"google.golang.org/protobuf/types/known/timestamppb"
    36  
    37  	"go.chromium.org/luci/appengine/gaetesting"
    38  	"go.chromium.org/luci/auth/identity"
    39  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    40  	"go.chromium.org/luci/common/clock"
    41  	"go.chromium.org/luci/common/clock/testclock"
    42  	"go.chromium.org/luci/common/sync/parallel"
    43  	"go.chromium.org/luci/gae/impl/memory"
    44  	"go.chromium.org/luci/gae/service/datastore"
    45  	"go.chromium.org/luci/server/auth"
    46  	"go.chromium.org/luci/server/auth/authtest"
    47  	"go.chromium.org/luci/server/caching"
    48  	"go.chromium.org/luci/server/redisconn"
    49  	"go.chromium.org/luci/server/router"
    50  
    51  	"go.chromium.org/luci/milo/internal/model"
    52  	"go.chromium.org/luci/milo/internal/model/milostatus"
    53  	"go.chromium.org/luci/milo/internal/projectconfig"
    54  	"go.chromium.org/luci/milo/internal/utils"
    55  
    56  	. "github.com/smartystreets/goconvey/convey"
    57  	. "go.chromium.org/luci/common/testing/assertions"
    58  )
    59  
    60  func newMockClient(c context.Context, t *testing.T) (context.Context, *gomock.Controller, *buildbucketpb.MockBuildsClient) {
    61  	ctrl := gomock.NewController(t)
    62  	client := buildbucketpb.NewMockBuildsClient(ctrl)
    63  	factory := func(c context.Context, host string, as auth.RPCAuthorityKind, opts ...auth.RPCOption) (buildbucketpb.BuildsClient, error) {
    64  		return client, nil
    65  	}
    66  	return WithBuildsClientFactory(c, factory), ctrl, client
    67  }
    68  
    69  // Buildbucket timestamps round off to milliseconds, so define a reference.
    70  var RefTime = time.Date(2016, time.February, 3, 4, 5, 6, 0, time.UTC)
    71  
    72  func makeReq(build *buildbucketpb.Build) io.ReadCloser {
    73  	bmsg := &buildbucketpb.BuildsV2PubSub{Build: build}
    74  	bm, _ := protojson.Marshal(bmsg)
    75  
    76  	msg := utils.PubSubSubscription{
    77  		Message: utils.PubSubMessage{
    78  			Data: base64.StdEncoding.EncodeToString(bm),
    79  		},
    80  	}
    81  	jmsg, _ := json.Marshal(msg)
    82  	return io.NopCloser(bytes.NewReader(jmsg))
    83  }
    84  
    85  func TestV2PubSub(t *testing.T) {
    86  	t.Parallel()
    87  
    88  	Convey(`TestV2PubSub`, t, func() {
    89  		c := gaetesting.TestingContextWithAppID("luci-milo-dev")
    90  		datastore.GetTestable(c).Consistent(true)
    91  		c, _ = testclock.UseTime(c, RefTime)
    92  		c = auth.WithState(c, &authtest.FakeState{
    93  			Identity:       identity.AnonymousIdentity,
    94  			IdentityGroups: []string{"all"},
    95  		})
    96  		c = caching.WithRequestCache(c)
    97  
    98  		// Initialize the appropriate builder.
    99  		builderSummary := &model.BuilderSummary{
   100  			BuilderID: "buildbucket/luci.fake.bucket/fake_builder",
   101  		}
   102  		err := datastore.Put(c, builderSummary)
   103  		So(err, ShouldBeNil)
   104  
   105  		// Initialize the appropriate project config.
   106  		err = datastore.Put(c, &projectconfig.Project{
   107  			ID: "fake",
   108  		})
   109  		So(err, ShouldBeNil)
   110  
   111  		// We'll copy this LegacyApiCommonBuildMessage base for convenience.
   112  		buildBase := &buildbucketpb.Build{
   113  			Builder: &buildbucketpb.BuilderID{
   114  				Project: "fake",
   115  				Bucket:  "bucket",
   116  				Builder: "fake_builder",
   117  			},
   118  			Infra: &buildbucketpb.BuildInfra{
   119  				Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{
   120  					Hostname: "hostname",
   121  				},
   122  			},
   123  			Input:      &buildbucketpb.Build_Input{},
   124  			Output:     &buildbucketpb.Build_Output{},
   125  			CreatedBy:  string(identity.AnonymousIdentity),
   126  			CreateTime: timestamppb.New(RefTime.Add(2 * time.Hour)),
   127  		}
   128  
   129  		Convey("New in-process build", func() {
   130  			bKey := model.MakeBuildKey(c, "hostname", "1234")
   131  			buildExp := proto.Clone(buildBase).(*buildbucketpb.Build)
   132  			buildExp.Id = 1234
   133  			buildExp.Status = buildbucketpb.Status_STARTED
   134  			buildExp.CreateTime = timestamppb.New(RefTime.Add(2 * time.Hour))
   135  			buildExp.StartTime = timestamppb.New(RefTime.Add(3 * time.Hour))
   136  			buildExp.UpdateTime = timestamppb.New(RefTime.Add(5 * time.Hour))
   137  			buildExp.Input.Experimental = true
   138  			propertiesMap := map[string]any{
   139  				"$recipe_engine/milo/blamelist_pins": []any{
   140  					map[string]any{
   141  						"host":    "chromium.googlesource.com",
   142  						"id":      "8930f18245df678abc944376372c77ba5e2a658b",
   143  						"project": "angle/angle",
   144  					},
   145  					map[string]any{
   146  						"host":    "chromium.googlesource.com",
   147  						"id":      "07033c702f81a75dfc2d83888ba3f8b354d0e920",
   148  						"project": "chromium/src",
   149  					},
   150  				},
   151  			}
   152  			buildExp.Output.Properties, _ = structpb.NewStruct(propertiesMap)
   153  
   154  			h := httptest.NewRecorder()
   155  			r := &http.Request{Body: makeReq(buildExp)}
   156  			V2PubSubHandler(&router.Context{
   157  				Writer:  h,
   158  				Request: r.WithContext(c),
   159  			})
   160  			So(h.Code, ShouldEqual, 200)
   161  			datastore.GetTestable(c).CatchupIndexes()
   162  
   163  			Convey("stores BuildSummary and BuilderSummary", func() {
   164  				buildAct := model.BuildSummary{BuildKey: bKey}
   165  				err := datastore.Get(c, &buildAct)
   166  				So(err, ShouldBeNil)
   167  				So(buildAct.BuildKey.String(), ShouldEqual, bKey.String())
   168  				So(buildAct.BuilderID, ShouldEqual, "buildbucket/luci.fake.bucket/fake_builder")
   169  				So(buildAct.Summary, ShouldResemble, model.Summary{
   170  					Status: milostatus.Running,
   171  					Start:  RefTime.Add(3 * time.Hour),
   172  				})
   173  				So(buildAct.Created, ShouldResemble, RefTime.Add(2*time.Hour))
   174  				So(buildAct.Experimental, ShouldBeTrue)
   175  				So(buildAct.BlamelistPins, ShouldResemble, []string{
   176  					"commit/gitiles/chromium.googlesource.com/angle/angle/+/8930f18245df678abc944376372c77ba5e2a658b",
   177  					"commit/gitiles/chromium.googlesource.com/chromium/src/+/07033c702f81a75dfc2d83888ba3f8b354d0e920",
   178  				})
   179  
   180  				blder := model.BuilderSummary{BuilderID: "buildbucket/luci.fake.bucket/fake_builder"}
   181  				err = datastore.Get(c, &blder)
   182  				So(err, ShouldBeNil)
   183  				So(blder.LastFinishedStatus, ShouldResemble, milostatus.NotRun)
   184  				So(blder.LastFinishedBuildID, ShouldEqual, "")
   185  			})
   186  		})
   187  
   188  		Convey("Completed build", func() {
   189  			bKey := model.MakeBuildKey(c, "hostname", "2234")
   190  			buildExp := buildBase
   191  			buildExp.Id = 2234
   192  			buildExp.Status = buildbucketpb.Status_SUCCESS
   193  			buildExp.CreateTime = timestamppb.New(RefTime.Add(2 * time.Hour))
   194  			buildExp.StartTime = timestamppb.New(RefTime.Add(3 * time.Hour))
   195  			buildExp.UpdateTime = timestamppb.New(RefTime.Add(6 * time.Hour))
   196  			buildExp.EndTime = timestamppb.New(RefTime.Add(6 * time.Hour))
   197  			buildExp.Input.GitilesCommit = &buildbucketpb.GitilesCommit{
   198  				Host:    "chromium.googlesource.com",
   199  				Id:      "8930f18245df678abc944376372c77ba5e2a658b",
   200  				Project: "angle/angle",
   201  			}
   202  
   203  			h := httptest.NewRecorder()
   204  			r := &http.Request{Body: makeReq(buildExp)}
   205  			V2PubSubHandler(&router.Context{
   206  				Writer:  h,
   207  				Request: r.WithContext(c),
   208  			})
   209  			So(h.Code, ShouldEqual, 200)
   210  
   211  			Convey("stores BuildSummary and BuilderSummary", func() {
   212  				buildAct := model.BuildSummary{BuildKey: bKey}
   213  				err := datastore.Get(c, &buildAct)
   214  				So(err, ShouldBeNil)
   215  				So(buildAct.BuildKey.String(), ShouldEqual, bKey.String())
   216  				So(buildAct.BuilderID, ShouldEqual, "buildbucket/luci.fake.bucket/fake_builder")
   217  				So(buildAct.Summary, ShouldResemble, model.Summary{
   218  					Status: milostatus.Success,
   219  					Start:  RefTime.Add(3 * time.Hour),
   220  					End:    RefTime.Add(6 * time.Hour),
   221  				})
   222  				So(buildAct.Created, ShouldResemble, RefTime.Add(2*time.Hour))
   223  
   224  				blder := model.BuilderSummary{BuilderID: "buildbucket/luci.fake.bucket/fake_builder"}
   225  				err = datastore.Get(c, &blder)
   226  				So(err, ShouldBeNil)
   227  				So(blder.LastFinishedCreated, ShouldResemble, RefTime.Add(2*time.Hour))
   228  				So(blder.LastFinishedStatus, ShouldResemble, milostatus.Success)
   229  				So(blder.LastFinishedBuildID, ShouldEqual, "buildbucket/2234")
   230  				So(buildAct.BlamelistPins, ShouldResemble, []string{
   231  					"commit/gitiles/chromium.googlesource.com/angle/angle/+/8930f18245df678abc944376372c77ba5e2a658b",
   232  				})
   233  			})
   234  
   235  			Convey("results in earlier update not being ingested", func() {
   236  				eBuild := &buildbucketpb.Build{
   237  					Id: 2234,
   238  					Builder: &buildbucketpb.BuilderID{
   239  						Project: "fake",
   240  						Bucket:  "bucket",
   241  						Builder: "fake_builder",
   242  					},
   243  					Infra: &buildbucketpb.BuildInfra{
   244  						Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{
   245  							Hostname: "hostname",
   246  						},
   247  					},
   248  					CreatedBy:  string(identity.AnonymousIdentity),
   249  					CreateTime: timestamppb.New(RefTime.Add(2 * time.Hour)),
   250  					StartTime:  timestamppb.New(RefTime.Add(3 * time.Hour)),
   251  					UpdateTime: timestamppb.New(RefTime.Add(4 * time.Hour)),
   252  					Status:     buildbucketpb.Status_STARTED,
   253  				}
   254  
   255  				h := httptest.NewRecorder()
   256  				r := &http.Request{Body: makeReq(eBuild)}
   257  				V2PubSubHandler(&router.Context{
   258  					Writer:  h,
   259  					Request: r.WithContext(c),
   260  				})
   261  				So(h.Code, ShouldEqual, 200)
   262  
   263  				buildAct := model.BuildSummary{BuildKey: bKey}
   264  				err := datastore.Get(c, &buildAct)
   265  				So(err, ShouldBeNil)
   266  				So(buildAct.Summary, ShouldResemble, model.Summary{
   267  					Status: milostatus.Success,
   268  					Start:  RefTime.Add(3 * time.Hour),
   269  					End:    RefTime.Add(6 * time.Hour),
   270  				})
   271  				So(buildAct.Created, ShouldResemble, RefTime.Add(2*time.Hour))
   272  
   273  				blder := model.BuilderSummary{BuilderID: "buildbucket/luci.fake.bucket/fake_builder"}
   274  				err = datastore.Get(c, &blder)
   275  				So(err, ShouldBeNil)
   276  				So(blder.LastFinishedCreated, ShouldResemble, RefTime.Add(2*time.Hour))
   277  				So(blder.LastFinishedStatus, ShouldResemble, milostatus.Success)
   278  				So(blder.LastFinishedBuildID, ShouldEqual, "buildbucket/2234")
   279  			})
   280  		})
   281  	})
   282  }
   283  
   284  func TestShouldUpdateBuilderSummary(t *testing.T) {
   285  	Convey("TestShouldUpdateBuilderSummary", t, func() {
   286  		c := context.Background()
   287  		startTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC).
   288  			Truncate(time.Duration(entityUpdateIntervalInS) * time.Second)
   289  
   290  		// Set up a test redis server.
   291  		s, err := miniredis.Run()
   292  		So(err, ShouldBeNil)
   293  		defer s.Close()
   294  		c = redisconn.UsePool(c, &redis.Pool{
   295  			Dial: func() (redis.Conn, error) {
   296  				return redis.Dial("tcp", s.Addr())
   297  			},
   298  		})
   299  
   300  		createBuildSummary := func(builderID string, status buildbucketpb.Status, createdAt time.Time) *model.BuildSummary {
   301  			return &model.BuildSummary{
   302  				BuilderID: builderID,
   303  				Summary: model.Summary{
   304  					Status: milostatus.FromBuildbucket(status),
   305  				},
   306  				Created: createdAt,
   307  			}
   308  		}
   309  
   310  		Convey("Single call", func() {
   311  			// Ensures `shouldUpdateBuilderSummary` is called at the start of the time bucket.
   312  			c, _ := testclock.UseTime(c, startTime)
   313  
   314  			start := clock.Now(c)
   315  			// Should return without advancing the clock.
   316  			shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-1", buildbucketpb.Status_SUCCESS, start))
   317  			So(err, ShouldBeNil)
   318  			So(shouldUpdate, ShouldBeTrue)
   319  		})
   320  
   321  		Convey("Single call followed by multiple parallel calls", func(tc C) {
   322  			// Ensures all `shouldUpdateBuilderSummary` calls are in the same time bucket.
   323  			c, tClock := testclock.UseTime(c, startTime)
   324  
   325  			pivot := clock.Now(c).Add(-time.Hour)
   326  
   327  			shouldUpdates := make([]bool, 4)
   328  
   329  			shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, pivot))
   330  			So(err, ShouldBeNil)
   331  			shouldUpdates[0] = shouldUpdate
   332  
   333  			err = parallel.FanOutIn(func(tasks chan<- func() error) {
   334  				eventC := make(chan string)
   335  				defer close(eventC)
   336  				tClock.SetTimerCallback(func(d time.Duration, t clock.Timer) {
   337  					eventC <- "timer"
   338  				})
   339  
   340  				tasks <- func() error {
   341  					createdAt := pivot.Add(5 * time.Millisecond)
   342  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, createdAt))
   343  					shouldUpdates[1] = shouldUpdate
   344  					return err
   345  				}
   346  
   347  				// Wait until the previous call reaches a blocking point.
   348  				tc.So(<-eventC, ShouldEqual, "timer")
   349  				tasks <- func() error {
   350  					createdAt := pivot.Add(15 * time.Millisecond)
   351  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, createdAt))
   352  					shouldUpdates[2] = shouldUpdate
   353  					return err
   354  				}
   355  
   356  				// Wait until the previous call reaches a blocking point.
   357  				tc.So(<-eventC, ShouldEqual, "timer")
   358  				tasks <- func() error {
   359  					createdAt := pivot.Add(10 * time.Millisecond)
   360  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-2", buildbucketpb.Status_SUCCESS, createdAt))
   361  					shouldUpdates[3] = shouldUpdate
   362  					eventC <- "return"
   363  					return err
   364  				}
   365  
   366  				// Wait until the last shouldUpdateBuilderSummary call returns then
   367  				// advance the clock to the next time bucket.
   368  				tc.So(<-eventC, ShouldEqual, "return")
   369  				tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second)
   370  			})
   371  			So(err, ShouldBeNil)
   372  
   373  			// The first value should be true because there's no recent updates.
   374  			So(shouldUpdates[0], ShouldBeTrue)
   375  
   376  			// The second value should be false because there's a recent update, so it
   377  			// moves to the next time bucket, wait for the timebucket to begin.
   378  			// Then it's replaced by the next shouldUpdateBuilderSummary call.
   379  			So(shouldUpdates[1], ShouldBeFalse)
   380  
   381  			// The third value should be true because it has a newer build than the
   382  			// current one. And it's not replaced by any new builds.
   383  			So(shouldUpdates[2], ShouldBeTrue)
   384  
   385  			// The forth value should be false because the build is not created earlier
   386  			// than the build associated with the current pending update in it's pending bucket.
   387  			So(shouldUpdates[3], ShouldBeFalse)
   388  		})
   389  
   390  		Convey("Single call followed by multiple parallel calls that are nanoseconds apart", func(tc C) {
   391  			// This test ensures that the timestamp percision is not lost.
   392  
   393  			// Ensures all `shouldUpdateBuilderSummary` calls are in the same time bucket.
   394  			c, tClock := testclock.UseTime(c, startTime)
   395  
   396  			pivot := clock.Now(c).Add(-time.Hour)
   397  
   398  			shouldUpdates := make([]bool, 4)
   399  
   400  			shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, pivot))
   401  			So(err, ShouldBeNil)
   402  			shouldUpdates[0] = shouldUpdate
   403  
   404  			err = parallel.FanOutIn(func(tasks chan<- func() error) {
   405  				eventC := make(chan string)
   406  				defer close(eventC)
   407  				tClock.SetTimerCallback(func(d time.Duration, t clock.Timer) {
   408  					eventC <- "timer"
   409  				})
   410  
   411  				tasks <- func() error {
   412  					createdAt := pivot.Add(time.Nanosecond)
   413  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, createdAt))
   414  					shouldUpdates[1] = shouldUpdate
   415  					return err
   416  				}
   417  
   418  				// Wait until the previous call reaches a blocking point.
   419  				tc.So(<-eventC, ShouldEqual, "timer")
   420  				tasks <- func() error {
   421  					createdAt := pivot.Add(3 * time.Nanosecond)
   422  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, createdAt))
   423  					shouldUpdates[2] = shouldUpdate
   424  					return err
   425  				}
   426  
   427  				// Wait until the previous call reaches a blocking point.
   428  				tc.So(<-eventC, ShouldEqual, "timer")
   429  				tasks <- func() error {
   430  					createdAt := pivot.Add(2 * time.Nanosecond)
   431  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-3", buildbucketpb.Status_SUCCESS, createdAt))
   432  					shouldUpdates[3] = shouldUpdate
   433  					eventC <- "return"
   434  					return err
   435  				}
   436  
   437  				// Wait until the last shouldUpdateBuilderSummary call returns then
   438  				// advance the clock to the next time bucket.
   439  				tc.So(<-eventC, ShouldEqual, "return")
   440  				tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second)
   441  			})
   442  			So(err, ShouldBeNil)
   443  
   444  			// The first value should be true because there's no pending/recent updates.
   445  			So(shouldUpdates[0], ShouldBeTrue)
   446  
   447  			// The second value should be false because there's a recent update, so it
   448  			// moves to the next time bucket, wait for the timebucket to begin.
   449  			// Then it's replaced by the next shouldUpdateBuilderSummary call.
   450  			So(shouldUpdates[1], ShouldBeFalse)
   451  
   452  			// The third value should be true because it has a newer build than the
   453  			// current pending one. And it's not replaced by any new builds.
   454  			So(shouldUpdates[2], ShouldBeTrue)
   455  
   456  			// The forth value should be false because the build is not created earlier
   457  			// than the build associated with the current pending update in it's pending bucket.
   458  			So(shouldUpdates[3], ShouldBeFalse)
   459  		})
   460  
   461  		Convey("Single call followed by multiple parallel calls in different time buckets", func(tc C) {
   462  			c, tClock := testclock.UseTime(c, startTime)
   463  
   464  			pivot := clock.Now(c).Add(-time.Hour)
   465  			shouldUpdates := make([]bool, 4)
   466  
   467  			shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, pivot))
   468  			So(err, ShouldBeNil)
   469  			shouldUpdates[0] = shouldUpdate
   470  
   471  			// Ensures the following `shouldUpdateBuilderSummary` calls are in a
   472  			// different time bucket.
   473  			tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second)
   474  
   475  			err = parallel.FanOutIn(func(tasks chan<- func() error) {
   476  				eventC := make(chan string)
   477  				defer close(eventC)
   478  				tClock.SetTimerCallback(func(d time.Duration, t clock.Timer) {
   479  					eventC <- "timer"
   480  				})
   481  
   482  				tasks <- func() error {
   483  					createdAt := pivot.Add(5 * time.Millisecond)
   484  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, createdAt))
   485  					shouldUpdates[1] = shouldUpdate
   486  					eventC <- "return"
   487  					return err
   488  				}
   489  
   490  				// Wait until the previous shouldUpdateBuilderSummary call returns.
   491  				tc.So(<-eventC, ShouldEqual, "return")
   492  				tasks <- func() error {
   493  					createdAt := pivot.Add(15 * time.Millisecond)
   494  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, createdAt))
   495  					shouldUpdates[2] = shouldUpdate
   496  					return err
   497  				}
   498  
   499  				// Wait until the previous call reaches a blocking point.
   500  				tc.So(<-eventC, ShouldEqual, "timer")
   501  				tasks <- func() error {
   502  					createdAt := pivot.Add(20 * time.Millisecond)
   503  					shouldUpdate, err := shouldUpdateBuilderSummary(c, createBuildSummary("test-builder-id-4", buildbucketpb.Status_SUCCESS, createdAt))
   504  					shouldUpdates[3] = shouldUpdate
   505  					return err
   506  				}
   507  
   508  				// Wait until the last shouldUpdateBuilderSummary call returns then
   509  				// advance the clock to the next time bucket.
   510  				tc.So(<-eventC, ShouldEqual, "timer")
   511  				tClock.Add(time.Duration(entityUpdateIntervalInS) * time.Second)
   512  			})
   513  			So(err, ShouldBeNil)
   514  
   515  			// The first value should be true because there's no pending/recent updates.
   516  			So(shouldUpdates[0], ShouldBeTrue)
   517  
   518  			// The second value should be true because it has move to a new time bucket
   519  			// and there's no pending/recent updates in that bucket.
   520  			So(shouldUpdates[1], ShouldBeTrue)
   521  
   522  			// The third value should be false because there's a recent update, so it
   523  			// moves to the next time bucket, wait for the timebucket to begin.
   524  			// Then it's replaced by the next shouldUpdateBuilderSummary call.
   525  			So(shouldUpdates[2], ShouldBeFalse)
   526  
   527  			// The forth value should be true because it has a newer build than the
   528  			// current pending one. And it's not replaced by any new builds.
   529  			So(shouldUpdates[3], ShouldBeTrue)
   530  		})
   531  	})
   532  }
   533  
   534  func TestDeleteOldBuilds(t *testing.T) {
   535  	t.Parallel()
   536  
   537  	Convey("DeleteOldBuilds", t, func() {
   538  		now := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC)
   539  
   540  		ctx, _ := testclock.UseTime(memory.Use(context.Background()), now)
   541  		datastore.GetTestable(ctx).AutoIndex(true)
   542  		datastore.GetTestable(ctx).Consistent(true)
   543  
   544  		createBuild := func(id string, t time.Time) *model.BuildSummary {
   545  			b := &model.BuildSummary{
   546  				BuildKey: model.MakeBuildKey(ctx, "host", id),
   547  				Created:  t,
   548  			}
   549  			So(datastore.Put(ctx, b), ShouldBeNil)
   550  			return b
   551  		}
   552  
   553  		Convey("keeps builds", func() {
   554  			Convey("as old as BuildSummaryStorageDuration", func() {
   555  				build := createBuild("1", now.Add(-BuildSummaryStorageDuration))
   556  				So(DeleteOldBuilds(ctx), ShouldBeNil)
   557  				So(datastore.Get(ctx, build), ShouldBeNil)
   558  			})
   559  			Convey("younger than BuildSummaryStorageDuration", func() {
   560  				build := createBuild("2", now.Add(-BuildSummaryStorageDuration+time.Minute))
   561  				So(DeleteOldBuilds(ctx), ShouldBeNil)
   562  				So(datastore.Get(ctx, build), ShouldBeNil)
   563  			})
   564  		})
   565  
   566  		Convey("deletes builds older than BuildSummaryStorageDuration", func() {
   567  			build := createBuild("3", now.Add(-BuildSummaryStorageDuration-time.Minute))
   568  			So(DeleteOldBuilds(ctx), ShouldBeNil)
   569  			So(datastore.Get(ctx, build), ShouldEqual, datastore.ErrNoSuchEntity)
   570  		})
   571  
   572  		Convey("removes many builds", func() {
   573  			bs := make([]*model.BuildSummary, 234)
   574  			old := now.Add(-BuildSummaryStorageDuration - time.Minute)
   575  			for i := range bs {
   576  				bs[i] = createBuild(fmt.Sprintf("4-%d", i), old)
   577  			}
   578  			So(DeleteOldBuilds(ctx), ShouldBeNil)
   579  			So(datastore.Get(ctx, bs), ShouldErrLike,
   580  				"datastore: no such entity (and 233 other errors)")
   581  		})
   582  	})
   583  }
   584  
   585  func TestSyncBuilds(t *testing.T) {
   586  	t.Parallel()
   587  
   588  	Convey("SyncBuilds", t, func() {
   589  		now := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC)
   590  
   591  		c, _ := testclock.UseTime(memory.Use(context.Background()), now)
   592  		datastore.GetTestable(c).AutoIndex(true)
   593  		datastore.GetTestable(c).Consistent(true)
   594  
   595  		createBuild := func(id string, t time.Time, status milostatus.Status) *model.BuildSummary {
   596  			b := &model.BuildSummary{
   597  				BuildKey:  model.MakeBuildKey(c, "host", id),
   598  				BuilderID: "buildbucket/luci.proj.bucket/builder",
   599  				BuildID:   "buildbucket/" + id,
   600  				Created:   t,
   601  				Summary: model.Summary{
   602  					Status: status,
   603  				},
   604  				Version: t.UnixNano(),
   605  			}
   606  			So(datastore.Put(c, b), ShouldBeNil)
   607  			return b
   608  		}
   609  
   610  		Convey("don't update builds", func() {
   611  			Convey("as old as BuildSummaryStorageDuration", func() {
   612  				build := createBuild("luci.proj.bucket/builder/1234", now.Add(-BuildSummarySyncThreshold), milostatus.Running)
   613  				So(syncBuildsImpl(c), ShouldBeNil)
   614  				So(datastore.Get(c, build), ShouldBeNil)
   615  				So(build.Summary.Status, ShouldEqual, milostatus.Running)
   616  			})
   617  
   618  			Convey("younger than BuildSummaryStorageDuration", func() {
   619  				build := createBuild("luci.proj.bucket/builder/1234", now.Add(-BuildSummarySyncThreshold+time.Minute), milostatus.NotRun)
   620  				So(syncBuildsImpl(c), ShouldBeNil)
   621  				So(datastore.Get(c, build), ShouldBeNil)
   622  				So(build.Summary.Status, ShouldEqual, milostatus.NotRun)
   623  			})
   624  		})
   625  
   626  		Convey("update builds older than BuildSummarySyncThreshold", func() {
   627  			build := createBuild("luci.proj.bucket/builder/1234", now.Add(-BuildSummarySyncThreshold-time.Minute), milostatus.NotRun)
   628  
   629  			c, ctrl, mbc := newMockClient(c, t)
   630  			defer ctrl.Finish()
   631  			mbc.EXPECT().GetBuild(gomock.Any(), gomock.Any()).Return(&buildbucketpb.Build{
   632  				Number: 1234,
   633  				Builder: &buildbucketpb.BuilderID{
   634  					Project: "proj",
   635  					Bucket:  "bucket",
   636  					Builder: "builder",
   637  				},
   638  				Status:     buildbucketpb.Status_SUCCESS,
   639  				CreateTime: timestamppb.New(build.Created),
   640  				UpdateTime: timestamppb.New(build.Created.Add(time.Hour)),
   641  			}, nil).AnyTimes()
   642  
   643  			So(syncBuildsImpl(c), ShouldBeNil)
   644  			So(datastore.Get(c, build), ShouldBeNil)
   645  			So(build.Summary.Status, ShouldEqual, milostatus.Success)
   646  		})
   647  
   648  		Convey("ensure BuildKey stays the same", func() {
   649  			build := createBuild("123456", now.Add(-BuildSummarySyncThreshold-time.Minute), milostatus.NotRun)
   650  
   651  			c, ctrl, mbc := newMockClient(c, t)
   652  			defer ctrl.Finish()
   653  			mbc.EXPECT().GetBuild(gomock.Any(), gomock.Any()).Return(&buildbucketpb.Build{
   654  				Id:     123456,
   655  				Number: 1234,
   656  				Builder: &buildbucketpb.BuilderID{
   657  					Project: "proj",
   658  					Bucket:  "bucket",
   659  					Builder: "builder",
   660  				},
   661  				Status:     buildbucketpb.Status_SUCCESS,
   662  				CreateTime: timestamppb.New(build.Created),
   663  				UpdateTime: timestamppb.New(build.Created.Add(time.Hour)),
   664  			}, nil).AnyTimes()
   665  
   666  			So(syncBuildsImpl(c), ShouldBeNil)
   667  			So(datastore.Get(c, build), ShouldBeNil)
   668  			So(build.Summary.Status, ShouldEqual, milostatus.Success)
   669  
   670  			buildWithNewKey := &model.BuildSummary{
   671  				BuildKey: model.MakeBuildKey(c, "host", "luci.proj.bucket/builder/1234"),
   672  			}
   673  			So(datastore.Get(c, buildWithNewKey), ShouldEqual, datastore.ErrNoSuchEntity)
   674  		})
   675  	})
   676  }