go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/sync_builds_with_backend_tasks_test.go (about)

     1  // Copyright 2023 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 tasks
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/golang/mock/gomock"
    25  
    26  	"google.golang.org/genproto/googleapis/rpc/status"
    27  	"google.golang.org/grpc"
    28  	"google.golang.org/protobuf/types/known/timestamppb"
    29  
    30  	"go.chromium.org/luci/common/clock/testclock"
    31  	"go.chromium.org/luci/common/errors"
    32  	"go.chromium.org/luci/common/sync/parallel"
    33  	"go.chromium.org/luci/gae/filter/txndefer"
    34  	"go.chromium.org/luci/gae/impl/memory"
    35  	"go.chromium.org/luci/gae/service/datastore"
    36  	"go.chromium.org/luci/server/caching"
    37  	"go.chromium.org/luci/server/tq"
    38  
    39  	"go.chromium.org/luci/buildbucket/appengine/internal/clients"
    40  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    41  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    42  	"go.chromium.org/luci/buildbucket/appengine/model"
    43  	pb "go.chromium.org/luci/buildbucket/proto"
    44  
    45  	. "github.com/smartystreets/goconvey/convey"
    46  	. "go.chromium.org/luci/common/testing/assertions"
    47  )
    48  
    49  var shards int32 = 10
    50  
    51  const (
    52  	defaultUpdateID = 5
    53  	staleUpdateID   = 3
    54  	newUpdateID     = 10
    55  )
    56  
    57  // fakeFetchTasksResponse mocks the FetchTasks RPC.
    58  func fakeFetchTasksResponse(ctx context.Context, taskReq *pb.FetchTasksRequest, opts ...grpc.CallOption) (*pb.FetchTasksResponse, error) {
    59  	responses := make([]*pb.FetchTasksResponse_Response, 0, len(taskReq.TaskIds))
    60  	for _, tID := range taskReq.TaskIds {
    61  		responseStatus := pb.Status_STARTED
    62  		updateID := newUpdateID
    63  		switch {
    64  		case strings.HasSuffix(tID.Id, "all_fail"):
    65  			return nil, errors.Reason("idk, wanted to fail i guess :/").Err()
    66  		case strings.HasSuffix(tID.Id, "fail_me"):
    67  			responses = append(responses, &pb.FetchTasksResponse_Response{
    68  				Response: &pb.FetchTasksResponse_Response_Error{
    69  					Error: &status.Status{
    70  						Code:    500,
    71  						Message: fmt.Sprintf("could not find task for taskId: %s", tID.Id),
    72  					},
    73  				},
    74  			})
    75  			continue
    76  		case strings.HasSuffix(tID.Id, "ended"):
    77  			responseStatus = pb.Status_SUCCESS
    78  		case strings.HasSuffix(tID.Id, "stale"):
    79  			updateID = staleUpdateID
    80  		case strings.HasSuffix(tID.Id, "unchanged"):
    81  			updateID = defaultUpdateID
    82  		}
    83  		responses = append(responses, &pb.FetchTasksResponse_Response{
    84  			Response: &pb.FetchTasksResponse_Response_Task{
    85  				Task: &pb.Task{
    86  					Id:       tID,
    87  					Status:   responseStatus,
    88  					UpdateId: int64(updateID),
    89  				},
    90  			},
    91  		})
    92  	}
    93  	return &pb.FetchTasksResponse{Responses: responses}, nil
    94  }
    95  
    96  func prepEntities(ctx context.Context, bID int64, buildStatus, outputStatus, taskStatus pb.Status, tIDSuffix string, updateTime time.Time) *datastore.Key {
    97  	tID := ""
    98  	if tIDSuffix != "no_task" {
    99  		tID = fmt.Sprintf("task%d%s", bID, tIDSuffix)
   100  	}
   101  	b := &model.Build{
   102  		ID: bID,
   103  		Proto: &pb.Build{
   104  			Id: bID,
   105  			Builder: &pb.BuilderID{
   106  				Project: "project",
   107  				Bucket:  "bucket",
   108  				Builder: "builder",
   109  			},
   110  			Status:     buildStatus,
   111  			UpdateTime: timestamppb.New(updateTime),
   112  			Output: &pb.Build_Output{
   113  				Status: outputStatus,
   114  			},
   115  		},
   116  		Status:              buildStatus,
   117  		BackendTarget:       "swarming",
   118  		BackendSyncInterval: 5 * time.Minute,
   119  		Project:             "project",
   120  	}
   121  	b.GenerateNextBackendSyncTime(ctx, shards)
   122  	bk := datastore.KeyForObj(ctx, b)
   123  	inf := &model.BuildInfra{
   124  		Build: bk,
   125  		Proto: &pb.BuildInfra{
   126  			Backend: &pb.BuildInfra_Backend{
   127  				Task: &pb.Task{
   128  					Status: taskStatus,
   129  					Id: &pb.TaskID{
   130  						Id:     tID,
   131  						Target: "swarming",
   132  					},
   133  					Link:     "a link",
   134  					UpdateId: defaultUpdateID,
   135  				},
   136  			},
   137  		},
   138  	}
   139  	bs := &model.BuildStatus{
   140  		Build:  bk,
   141  		Status: buildStatus,
   142  	}
   143  	So(datastore.Put(ctx, b, inf, bs), ShouldBeNil)
   144  	return bk
   145  }
   146  
   147  func TestQueryBuildsToSync(t *testing.T) {
   148  	ctx := context.Background()
   149  	now := testclock.TestRecentTimeUTC
   150  	ctx, _ = testclock.UseTime(ctx, now)
   151  	ctx = caching.WithEmptyProcessCache(ctx)
   152  	ctx = memory.UseWithAppID(ctx, "dev~app-id")
   153  	ctx = txndefer.FilterRDS(ctx)
   154  	datastore.GetTestable(ctx).AutoIndex(true)
   155  	datastore.GetTestable(ctx).Consistent(true)
   156  
   157  	t.Parallel()
   158  
   159  	Convey("queryBuildsToSync", t, func() {
   160  		put := func(ctx context.Context, project, backend string, bID int64, status pb.Status, updateTime time.Time) {
   161  			b := &model.Build{
   162  				ID: bID,
   163  				Proto: &pb.Build{
   164  					Id: bID,
   165  					Builder: &pb.BuilderID{
   166  						Project: project,
   167  						Bucket:  "bucket",
   168  						Builder: "builder",
   169  					},
   170  					Status:     status,
   171  					UpdateTime: timestamppb.New(updateTime),
   172  				},
   173  				Status:              status,
   174  				Project:             project,
   175  				BackendTarget:       backend,
   176  				BackendSyncInterval: 5 * time.Minute,
   177  			}
   178  			b.GenerateNextBackendSyncTime(ctx, shards)
   179  			So(datastore.Put(ctx, b), ShouldBeNil)
   180  		}
   181  
   182  		project := "project"
   183  		backend := "swarming"
   184  
   185  		// Prepare build entities.
   186  		// Should be included in query results.
   187  		// updated 1 hour ago.
   188  		for i := 1; i <= 5; i++ {
   189  			put(ctx, project, backend, int64(i), pb.Status_STARTED, now.Add(-time.Hour))
   190  		}
   191  		// Should not be included in query results.
   192  		// Just updated.
   193  		put(ctx, project, backend, 6, pb.Status_STARTED, now)
   194  		// Different project.
   195  		put(ctx, "another_project", backend, 7, pb.Status_STARTED, now.Add(-time.Hour))
   196  		// Different backend.
   197  		put(ctx, project, "another_backend", 8, pb.Status_STARTED, now.Add(-time.Hour))
   198  		// Build has completed.
   199  		put(ctx, project, backend, 9, pb.Status_SUCCESS, now.Add(-time.Hour))
   200  
   201  		var allBks []*datastore.Key
   202  		err := parallel.RunMulti(ctx, int(shards), func(mr parallel.MultiRunner) error {
   203  			bkC := make(chan []*datastore.Key)
   204  			return mr.RunMulti(func(work chan<- func() error) {
   205  				work <- func() error {
   206  					defer close(bkC)
   207  					return queryBuildsToSync(ctx, mr, backend, project, shards, now, bkC)
   208  				}
   209  
   210  				for bks := range bkC {
   211  					bks := bks
   212  					allBks = append(allBks, bks...)
   213  				}
   214  			})
   215  		})
   216  		So(err, ShouldBeNil)
   217  		So(len(allBks), ShouldEqual, 5)
   218  	})
   219  }
   220  
   221  func TestSyncBuildsWithBackendTasksOneFetchBatch(t *testing.T) {
   222  	ctl := gomock.NewController(t)
   223  	defer ctl.Finish()
   224  	ctx := context.Background()
   225  	mockBackend := clients.NewMockTaskBackendClient(ctl)
   226  	mockBackend.EXPECT().FetchTasks(gomock.Any(), gomock.Any()).DoAndReturn(fakeFetchTasksResponse).AnyTimes()
   227  	now := testclock.TestRecentTimeUTC
   228  	ctx, _ = testclock.UseTime(ctx, now)
   229  	ctx = context.WithValue(ctx, clients.MockTaskBackendClientKey, mockBackend)
   230  	ctx = caching.WithEmptyProcessCache(ctx)
   231  	ctx = memory.UseWithAppID(ctx, "dev~app-id")
   232  	ctx = txndefer.FilterRDS(ctx)
   233  	ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   234  	datastore.GetTestable(ctx).AutoIndex(true)
   235  	datastore.GetTestable(ctx).Consistent(true)
   236  
   237  	getEntities := func(bIDs []int64) []*model.Build {
   238  		var blds []*model.Build
   239  		for _, id := range bIDs {
   240  			blds = append(blds, &model.Build{ID: id})
   241  		}
   242  		So(datastore.Get(ctx, blds), ShouldBeNil)
   243  		return blds
   244  	}
   245  
   246  	Convey("syncBuildsWithBackendTasks", t, func() {
   247  		ctx, sch := tq.TestingContext(ctx, nil)
   248  		backendSetting := []*pb.BackendSetting{
   249  			&pb.BackendSetting{
   250  				Target:   "swarming",
   251  				Hostname: "hostname",
   252  			},
   253  		}
   254  		settingsCfg := &pb.SettingsCfg{Backends: backendSetting}
   255  
   256  		bc, err := clients.NewBackendClient(ctx, "project", "swarming", settingsCfg)
   257  		So(err, ShouldBeNil)
   258  
   259  		sync := func(bks []*datastore.Key) error {
   260  			return parallel.RunMulti(ctx, 5, func(mr parallel.MultiRunner) error {
   261  				return mr.RunMulti(func(work chan<- func() error) {
   262  					work <- func() error {
   263  						return syncBuildsWithBackendTasks(ctx, mr, bc, bks, now)
   264  					}
   265  				})
   266  			})
   267  		}
   268  
   269  		Convey("nothing to update", func() {
   270  			updateTime := now.Add(-2 * time.Minute)
   271  			bIDs := []int64{3, 4, 5}
   272  			var bks []*datastore.Key
   273  			bks = append(bks, prepEntities(ctx, 3, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", updateTime))
   274  			bks = append(bks, prepEntities(ctx, 4, pb.Status_FAILURE, pb.Status_FAILURE, pb.Status_FAILURE, "", updateTime))
   275  			bks = append(bks, prepEntities(ctx, 5, pb.Status_SCHEDULED, pb.Status_SCHEDULED, pb.Status_SCHEDULED, "no_task", updateTime))
   276  			err := sync(bks)
   277  			So(err, ShouldBeNil)
   278  			So(sch.Tasks(), ShouldBeEmpty)
   279  			blds := getEntities(bIDs)
   280  			for _, b := range blds {
   281  				So(b.Proto.UpdateTime.AsTime(), ShouldEqual, updateTime)
   282  			}
   283  		})
   284  
   285  		Convey("ok", func() {
   286  			bIDs := []int64{1, 2}
   287  			var bks []*datastore.Key
   288  			for _, id := range bIDs {
   289  				bks = append(bks, prepEntities(ctx, id, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", now.Add(-time.Hour)))
   290  			}
   291  			err = sync(bks)
   292  			So(err, ShouldBeNil)
   293  			So(sch.Tasks(), ShouldBeEmpty)
   294  			blds := getEntities(bIDs)
   295  			for _, b := range blds {
   296  				So(b.Proto.UpdateTime.AsTime(), ShouldEqual, now)
   297  			}
   298  		})
   299  
   300  		Convey("ok end builds", func() {
   301  			bIDs := []int64{3, 4}
   302  			var bks []*datastore.Key
   303  			for _, id := range bIDs {
   304  				bks = append(bks, prepEntities(ctx, id, pb.Status_STARTED, pb.Status_SUCCESS, pb.Status_STARTED, "ended", now.Add(-time.Hour)))
   305  			}
   306  			updateBatchSize = 1 // To test update in multiple batches.
   307  
   308  			err := sync(bks)
   309  			So(err, ShouldBeNil)
   310  			// TQ tasks for pubsub-notification *2, bq-export, and invocation-finalization per build.
   311  			So(sch.Tasks(), ShouldHaveLength, 8)
   312  			blds := getEntities(bIDs)
   313  			for _, b := range blds {
   314  				So(b.Proto.UpdateTime.AsTime(), ShouldEqual, now)
   315  				So(b.Status, ShouldEqual, pb.Status_SUCCESS)
   316  			}
   317  		})
   318  
   319  		Convey("partially ok", func() {
   320  			preSyncUpdateTime := now.Add(-time.Hour)
   321  			bIDs := []int64{5, 6, 7, 8}
   322  			var bks []*datastore.Key
   323  			// build 5 is ok.
   324  			bks = append(bks, prepEntities(ctx, 5, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", preSyncUpdateTime))
   325  			// failed to get the task for build 6.
   326  			bks = append(bks, prepEntities(ctx, 6, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "fail_me", preSyncUpdateTime))
   327  			// task for build 7 is stale.
   328  			bks = append(bks, prepEntities(ctx, 7, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "stale", preSyncUpdateTime))
   329  			// task for build 8 is unchanged.
   330  			bks = append(bks, prepEntities(ctx, 8, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "unchanged", preSyncUpdateTime))
   331  
   332  			blds := getEntities(bIDs)
   333  			nextSyncTimeBeforeSync := blds[3].NextBackendSyncTime
   334  
   335  			err := sync(bks)
   336  			So(err, ShouldBeNil)
   337  			So(sch.Tasks(), ShouldBeEmpty)
   338  			blds = getEntities(bIDs)
   339  			// build 5 is updated with new update_id
   340  			So(blds[0].Proto.UpdateTime.AsTime(), ShouldEqual, now)
   341  			// build 6 is not updated due to failing to get the task
   342  			So(blds[1].Proto.UpdateTime.AsTime(), ShouldEqual, preSyncUpdateTime)
   343  			// build 7 has a stale updateID so it is not udpated
   344  			So(blds[2].Proto.UpdateTime.AsTime(), ShouldEqual, preSyncUpdateTime)
   345  			// build 8 is unchanged, but we still update the builds update time
   346  			So(blds[3].Proto.UpdateTime.AsTime(), ShouldEqual, now)
   347  			So(blds[3].NextBackendSyncTime, ShouldBeGreaterThan, nextSyncTimeBeforeSync)
   348  
   349  		})
   350  
   351  		Convey("all fail", func() {
   352  			preSyncUpdateTime := now.Add(-time.Hour)
   353  			bIDs := []int64{5, 6}
   354  			var bks []*datastore.Key
   355  			bks = append(bks, prepEntities(ctx, 5, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", preSyncUpdateTime))
   356  			bks = append(bks, prepEntities(ctx, 6, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "all_fail", preSyncUpdateTime))
   357  
   358  			err := sync(bks)
   359  			So(err, ShouldErrLike, "idk, wanted to fail i guess :/")
   360  			So(sch.Tasks(), ShouldBeEmpty)
   361  			blds := getEntities(bIDs)
   362  			for _, b := range blds {
   363  				So(b.Proto.UpdateTime.AsTime(), ShouldEqual, preSyncUpdateTime)
   364  			}
   365  		})
   366  	})
   367  }
   368  
   369  func TestSyncBuildsWithBackendTasks(t *testing.T) {
   370  	ctl := gomock.NewController(t)
   371  	defer ctl.Finish()
   372  	ctx := context.Background()
   373  	mockBackend := clients.NewMockTaskBackendClient(ctl)
   374  	mockBackend.EXPECT().FetchTasks(gomock.Any(), gomock.Any()).DoAndReturn(fakeFetchTasksResponse).AnyTimes()
   375  	now := testclock.TestRecentTimeUTC
   376  	ctx, _ = testclock.UseTime(ctx, now)
   377  	ctx = context.WithValue(ctx, clients.MockTaskBackendClientKey, mockBackend)
   378  	ctx = caching.WithEmptyProcessCache(ctx)
   379  	ctx = memory.UseWithAppID(ctx, "dev~app-id")
   380  	ctx = txndefer.FilterRDS(ctx)
   381  	ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   382  	datastore.GetTestable(ctx).AutoIndex(true)
   383  	datastore.GetTestable(ctx).Consistent(true)
   384  
   385  	getEntities := func(bIDs []int64) []*model.Build {
   386  		var blds []*model.Build
   387  		for _, id := range bIDs {
   388  			blds = append(blds, &model.Build{ID: id})
   389  		}
   390  		So(datastore.Get(ctx, blds), ShouldBeNil)
   391  		return blds
   392  	}
   393  
   394  	Convey("SyncBuildsWithBackendTasks", t, func() {
   395  		ctx, sch := tq.TestingContext(ctx, nil)
   396  		backendSetting := []*pb.BackendSetting{
   397  			{
   398  				Target:   "swarming",
   399  				Hostname: "hostname",
   400  				Mode: &pb.BackendSetting_FullMode_{
   401  					FullMode: &pb.BackendSetting_FullMode{
   402  						BuildSyncSetting: &pb.BackendSetting_BuildSyncSetting{
   403  							Shards: shards,
   404  						},
   405  					},
   406  				},
   407  			},
   408  			{
   409  				Target:   "foo",
   410  				Hostname: "foo_hostname",
   411  				Mode: &pb.BackendSetting_LiteMode_{
   412  					LiteMode: &pb.BackendSetting_LiteMode{},
   413  				},
   414  			},
   415  		}
   416  		settingsCfg := &pb.SettingsCfg{Backends: backendSetting}
   417  		err := config.SetTestSettingsCfg(ctx, settingsCfg)
   418  		So(err, ShouldBeNil)
   419  
   420  		Convey("ok - full mode", func() {
   421  			bIDs := []int64{101, 102, 103, 104, 105}
   422  			fetchBatchSize = 1
   423  			updateBatchSize = 1
   424  			for _, id := range bIDs {
   425  				prepEntities(ctx, id, pb.Status_STARTED, pb.Status_SUCCESS, pb.Status_STARTED, "", now.Add(-time.Hour))
   426  			}
   427  			prepEntities(ctx, 106, pb.Status_STARTED, pb.Status_SUCCESS, pb.Status_STARTED, "ended", now.Add(-time.Hour))
   428  			bIDs = append(bIDs, 106)
   429  			err = SyncBuildsWithBackendTasks(ctx, "swarming", "project")
   430  			So(err, ShouldBeNil)
   431  			So(sch.Tasks(), ShouldHaveLength, 4) // 106 completed
   432  			blds := getEntities(bIDs)
   433  			for _, b := range blds {
   434  				So(b.Proto.UpdateTime.AsTime(), ShouldEqual, now)
   435  				if b.ID == int64(106) {
   436  					So(b.Status, ShouldEqual, pb.Status_SUCCESS)
   437  				}
   438  			}
   439  		})
   440  
   441  		Convey("no sync - lite mode", func() {
   442  			err = SyncBuildsWithBackendTasks(ctx, "foo", "project")
   443  			So(err, ShouldBeNil)
   444  			So(sch.Tasks(), ShouldHaveLength, 0)
   445  		})
   446  
   447  		Convey("backend setting not found", func() {
   448  			err = SyncBuildsWithBackendTasks(ctx, "not_exist", "project")
   449  			So(err, ShouldErrLike, "failed to find backend not_exist from global config")
   450  		})
   451  	})
   452  }