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

     1  // Copyright 2022 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  	"io"
    21  	"net/http"
    22  	"net/http/httptest"
    23  	"strconv"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/golang/mock/gomock"
    29  
    30  	"google.golang.org/api/googleapi"
    31  	codepb "google.golang.org/genproto/googleapis/rpc/code"
    32  	statuspb "google.golang.org/genproto/googleapis/rpc/status"
    33  	"google.golang.org/grpc/codes"
    34  	"google.golang.org/protobuf/proto"
    35  	"google.golang.org/protobuf/types/known/durationpb"
    36  	"google.golang.org/protobuf/types/known/structpb"
    37  	"google.golang.org/protobuf/types/known/timestamppb"
    38  
    39  	cipdpb "go.chromium.org/luci/cipd/api/cipd/v1"
    40  	"go.chromium.org/luci/common/clock/testclock"
    41  	"go.chromium.org/luci/common/logging"
    42  	"go.chromium.org/luci/common/logging/memlogger"
    43  	"go.chromium.org/luci/common/retry"
    44  	"go.chromium.org/luci/gae/filter/txndefer"
    45  	"go.chromium.org/luci/gae/impl/memory"
    46  	"go.chromium.org/luci/gae/service/datastore"
    47  	"go.chromium.org/luci/grpc/prpc"
    48  	"go.chromium.org/luci/server/caching"
    49  	"go.chromium.org/luci/server/secrets"
    50  	"go.chromium.org/luci/server/secrets/testsecrets"
    51  	"go.chromium.org/luci/server/tq"
    52  
    53  	"go.chromium.org/luci/buildbucket/appengine/internal/clients"
    54  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    55  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    56  	"go.chromium.org/luci/buildbucket/appengine/model"
    57  	taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs"
    58  	pb "go.chromium.org/luci/buildbucket/proto"
    59  
    60  	. "github.com/smartystreets/goconvey/convey"
    61  	. "go.chromium.org/luci/common/testing/assertions"
    62  )
    63  
    64  // This will help track the number of times the cipd server is called to test if the cache is working as intended.
    65  var numCipdCalls int
    66  
    67  func describeBootstrapBundle(c C, hasBadPkgFile bool) http.HandlerFunc {
    68  	return func(w http.ResponseWriter, r *http.Request) {
    69  		c.So(r.URL.Path, ShouldEqual, "/prpc/cipd.Repository/DescribeBootstrapBundle")
    70  		numCipdCalls++
    71  		reqBody, err := io.ReadAll(r.Body)
    72  		c.So(err, ShouldBeNil)
    73  		req := &cipdpb.DescribeBootstrapBundleRequest{}
    74  		err = proto.Unmarshal(reqBody, req)
    75  		c.So(err, ShouldBeNil)
    76  		variants := []string{
    77  			"linux-amd64",
    78  			"mac-amd64",
    79  		}
    80  		bootstrapFiles := []*cipdpb.DescribeBootstrapBundleResponse_BootstrapFile{}
    81  		for _, variant := range variants {
    82  			pkdName := req.Prefix + "/" + variant
    83  			bootstrapFile := &cipdpb.DescribeBootstrapBundleResponse_BootstrapFile{
    84  				Package: pkdName,
    85  				Size:    100,
    86  				Instance: &cipdpb.ObjectRef{
    87  					HashAlgo:  cipdpb.HashAlgo_SHA256,
    88  					HexDigest: "this_is_a_sha_256_I_swear",
    89  				},
    90  			}
    91  			bootstrapFiles = append(bootstrapFiles, bootstrapFile)
    92  		}
    93  		if hasBadPkgFile {
    94  			bootstrapFiles = append(bootstrapFiles, &cipdpb.DescribeBootstrapBundleResponse_BootstrapFile{
    95  				Package: req.Prefix + "/" + "bad-platform",
    96  				Status: &statuspb.Status{
    97  					Code:    int32(codepb.Code_NOT_FOUND),
    98  					Message: "no such tag",
    99  				},
   100  			})
   101  		}
   102  		res := &cipdpb.DescribeBootstrapBundleResponse{
   103  			Files: bootstrapFiles,
   104  		}
   105  		var buf []byte
   106  		buf, _ = proto.Marshal(res)
   107  		code := codes.OK
   108  		status := http.StatusOK
   109  		w.Header().Set("Content-Type", r.Header.Get("Accept"))
   110  		w.Header().Set(prpc.HeaderGRPCCode, strconv.Itoa(int(code)))
   111  		w.WriteHeader(status)
   112  		_, err = w.Write(buf)
   113  		c.So(err, ShouldBeNil)
   114  	}
   115  }
   116  
   117  func helpTestCipdCall(c C, ctx context.Context, infra *pb.BuildInfra, expectedNumCalls int) {
   118  	m, err := extractCipdDetails(ctx, "project", infra)
   119  	c.So(err, ShouldBeNil)
   120  	detail, ok := m["infra/tools/luci/bbagent/linux-amd64"]
   121  	c.So(ok, ShouldBeTrue)
   122  	c.So(detail, ShouldResembleProto, &pb.RunTaskRequest_AgentExecutable_AgentSource{
   123  		Sha256:    "this_is_a_sha_256_I_swear",
   124  		SizeBytes: 100,
   125  		Url:       "https://chrome-infra-packages.appspot.com/bootstrap/infra/tools/luci/bbagent/linux-amd64/+/latest",
   126  	})
   127  	detail, ok = m["infra/tools/luci/bbagent/mac-amd64"]
   128  	c.So(ok, ShouldBeTrue)
   129  	c.So(detail, ShouldResembleProto, &pb.RunTaskRequest_AgentExecutable_AgentSource{
   130  		Sha256:    "this_is_a_sha_256_I_swear",
   131  		SizeBytes: 100,
   132  		Url:       "https://chrome-infra-packages.appspot.com/bootstrap/infra/tools/luci/bbagent/mac-amd64/+/latest",
   133  	})
   134  	c.So(numCipdCalls, ShouldEqual, expectedNumCalls)
   135  }
   136  
   137  func TestCipdClient(t *testing.T) {
   138  	t.Parallel()
   139  
   140  	Convey("extractCipdDetails", t, func(c C) {
   141  		now := testclock.TestRecentTimeUTC
   142  		ctx, _ := testclock.UseTime(context.Background(), now)
   143  		ctx = caching.WithEmptyProcessCache(ctx)
   144  		ctx = memory.UseWithAppID(ctx, "dev~app-id")
   145  		ctx = txndefer.FilterRDS(ctx)
   146  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   147  		datastore.GetTestable(ctx).AutoIndex(true)
   148  		datastore.GetTestable(ctx).Consistent(true)
   149  		ctx, _ = tq.TestingContext(ctx, nil)
   150  		mockCipdServer := httptest.NewServer(describeBootstrapBundle(c, false))
   151  		defer mockCipdServer.Close()
   152  		mockCipdClient := &prpc.Client{
   153  			Host: strings.TrimPrefix(mockCipdServer.URL, "http://"),
   154  			Options: &prpc.Options{
   155  				Retry: func() retry.Iterator {
   156  					return &retry.Limited{
   157  						Retries: 3,
   158  						Delay:   0,
   159  					}
   160  				},
   161  				Insecure:  true,
   162  				UserAgent: "prpc-test",
   163  			},
   164  		}
   165  		ctx = context.WithValue(ctx, MockCipdClientKey{}, mockCipdClient)
   166  
   167  		Convey("ok", func() {
   168  			infra := &pb.BuildInfra{
   169  				Backend: &pb.BuildInfra_Backend{
   170  					Task: &pb.Task{
   171  						Id: &pb.TaskID{
   172  							Id:     "abc123",
   173  							Target: "swarming://mytarget",
   174  						},
   175  					},
   176  				},
   177  				Buildbucket: &pb.BuildInfra_Buildbucket{
   178  					Agent: &pb.BuildInfra_Buildbucket_Agent{
   179  						Source: &pb.BuildInfra_Buildbucket_Agent_Source{
   180  							DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
   181  								Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
   182  									Package: "infra/tools/luci/bbagent/${platform}",
   183  									Version: "latest",
   184  									Server:  "https://chrome-infra-packages.appspot.com",
   185  								},
   186  							},
   187  						},
   188  					},
   189  					Hostname: "some unique host name",
   190  				},
   191  			}
   192  			Convey("no non-ok pkg file", func() {
   193  				numCipdCalls = 0
   194  				// call extractCipdDetails function 10 times.
   195  				// The test asserts that numCipdCalls should always be 1
   196  				for i := 0; i < 10; i++ {
   197  					helpTestCipdCall(c, ctx, infra, 1)
   198  				}
   199  			})
   200  			Convey("contain non-ok pkg file", func() {
   201  				mockCipdServer := httptest.NewServer(describeBootstrapBundle(c, true))
   202  				defer mockCipdServer.Close()
   203  				mockCipdClient := &prpc.Client{
   204  					Host: strings.TrimPrefix(mockCipdServer.URL, "http://"),
   205  					Options: &prpc.Options{
   206  						Retry: func() retry.Iterator {
   207  							return &retry.Limited{
   208  								Retries: 3,
   209  								Delay:   0,
   210  							}
   211  						},
   212  						Insecure:  true,
   213  						UserAgent: "prpc-test",
   214  					},
   215  				}
   216  				ctx = context.WithValue(ctx, MockCipdClientKey{}, mockCipdClient)
   217  				numCipdCalls = 0
   218  				helpTestCipdCall(c, ctx, infra, 1)
   219  				// make the cuurent time longer than the cache TTL.
   220  				ctx, _ = testclock.UseTime(ctx, now.Add(5*time.Minute))
   221  				helpTestCipdCall(c, ctx, infra, 2)
   222  			})
   223  		})
   224  	})
   225  }
   226  
   227  func TestCreateBackendTask(t *testing.T) {
   228  	Convey("computeBackendNewTaskReq", t, func(c C) {
   229  		ctl := gomock.NewController(t)
   230  		defer ctl.Finish()
   231  		mockTaskCreator := clients.NewMockTaskCreator(ctl)
   232  		now := testclock.TestRecentTimeUTC
   233  		ctx, _ := testclock.UseTime(context.Background(), now)
   234  		ctx = context.WithValue(ctx, clients.MockTaskCreatorKey, mockTaskCreator)
   235  		ctx = caching.WithEmptyProcessCache(ctx)
   236  		ctx = memory.UseWithAppID(ctx, "dev~app-id")
   237  		ctx = txndefer.FilterRDS(ctx)
   238  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   239  		datastore.GetTestable(ctx).AutoIndex(true)
   240  		datastore.GetTestable(ctx).Consistent(true)
   241  		ctx, _ = tq.TestingContext(ctx, nil)
   242  		store := &testsecrets.Store{
   243  			Secrets: map[string]secrets.Secret{
   244  				"key": {Active: []byte("stuff")},
   245  			},
   246  		}
   247  		ctx = secrets.Use(ctx, store)
   248  		ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx)
   249  
   250  		backendSetting := []*pb.BackendSetting{}
   251  		backendSetting = append(backendSetting, &pb.BackendSetting{
   252  			Target:   "swarming:/chromium-swarm-dev",
   253  			Hostname: "chromium-swarm-dev",
   254  			Mode: &pb.BackendSetting_FullMode_{
   255  				FullMode: &pb.BackendSetting_FullMode{
   256  					PubsubId: "chromium-swarm-dev-backend",
   257  				},
   258  			},
   259  		})
   260  		settingsCfg := &pb.SettingsCfg{Backends: backendSetting}
   261  		server := httptest.NewServer(describeBootstrapBundle(c, false))
   262  		defer server.Close()
   263  		client := &prpc.Client{
   264  			Host: strings.TrimPrefix(server.URL, "http://"),
   265  			Options: &prpc.Options{
   266  				Retry: func() retry.Iterator {
   267  					return &retry.Limited{
   268  						Retries: 3,
   269  						Delay:   0,
   270  					}
   271  				},
   272  				Insecure:  true,
   273  				UserAgent: "prpc-test",
   274  			},
   275  		}
   276  		ctx = context.WithValue(ctx, MockCipdClientKey{}, client)
   277  
   278  		Convey("ok", func() {
   279  			build := &model.Build{
   280  				ID:        1,
   281  				BucketID:  "project/bucket",
   282  				Project:   "project",
   283  				BuilderID: "project/bucket",
   284  				Proto: &pb.Build{
   285  					Id: 1,
   286  					Builder: &pb.BuilderID{
   287  						Builder: "builder",
   288  						Bucket:  "bucket",
   289  						Project: "project",
   290  					},
   291  					CreateTime: &timestamppb.Timestamp{
   292  						Seconds: 1677511793,
   293  					},
   294  					SchedulingTimeout: &durationpb.Duration{Seconds: 100},
   295  					ExecutionTimeout:  &durationpb.Duration{Seconds: 500},
   296  					Input: &pb.Build_Input{
   297  						Experiments: []string{
   298  							"cow_eggs_experiment",
   299  							"are_cow_eggs_real_experiment",
   300  						},
   301  					},
   302  					GracePeriod: &durationpb.Duration{Seconds: 50},
   303  				},
   304  			}
   305  			key := datastore.KeyForObj(ctx, build)
   306  			infra := &model.BuildInfra{
   307  				Build: key,
   308  				Proto: &pb.BuildInfra{
   309  					Backend: &pb.BuildInfra_Backend{
   310  						Caches: []*pb.CacheEntry{
   311  							{
   312  								Name: "cache_name",
   313  								Path: "cache_value",
   314  							},
   315  						},
   316  						Config: &structpb.Struct{
   317  							Fields: map[string]*structpb.Value{
   318  								"priority": {
   319  									Kind: &structpb.Value_NumberValue{NumberValue: 32},
   320  								},
   321  								"bot_ping_tolerance": {
   322  									Kind: &structpb.Value_NumberValue{NumberValue: 2},
   323  								},
   324  							},
   325  						},
   326  						Task: &pb.Task{
   327  							Id: &pb.TaskID{
   328  								Id:     "",
   329  								Target: "swarming:/chromium-swarm-dev",
   330  							},
   331  						},
   332  						TaskDimensions: []*pb.RequestedDimension{
   333  							{
   334  								Key:   "dim_key_1",
   335  								Value: "dim_val_1",
   336  							},
   337  						},
   338  					},
   339  					Bbagent: &pb.BuildInfra_BBAgent{
   340  						CacheDir: "cache",
   341  					},
   342  					Buildbucket: &pb.BuildInfra_Buildbucket{
   343  						Hostname: "some unique host name",
   344  						Agent: &pb.BuildInfra_Buildbucket_Agent{
   345  							Source: &pb.BuildInfra_Buildbucket_Agent_Source{
   346  								DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
   347  									Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
   348  										Package: "infra/tools/luci/bbagent/${platform}",
   349  										Version: "latest",
   350  										Server:  "https://chrome-infra-packages.appspot.com",
   351  									},
   352  								},
   353  							},
   354  							CipdClientCache: &pb.CacheEntry{
   355  								Name: "cipd_client_hash",
   356  								Path: "cipd_client",
   357  							},
   358  							CipdPackagesCache: &pb.CacheEntry{
   359  								Name: "cipd_cache_hash",
   360  								Path: "cipd_cache",
   361  							},
   362  						},
   363  					},
   364  				},
   365  			}
   366  			req, err := computeBackendNewTaskReq(ctx, build, infra, "request_id", settingsCfg)
   367  			So(err, ShouldBeNil)
   368  			So(req.BackendConfig, ShouldResembleProto, &structpb.Struct{
   369  				Fields: map[string]*structpb.Value{
   370  					"priority": {
   371  						Kind: &structpb.Value_NumberValue{NumberValue: 32},
   372  					},
   373  					"bot_ping_tolerance": {
   374  						Kind: &structpb.Value_NumberValue{NumberValue: 2},
   375  					},
   376  					"tags": {
   377  						Kind: &structpb.Value_ListValue{
   378  							ListValue: &structpb.ListValue{
   379  								Values: []*structpb.Value{
   380  									{Kind: &structpb.Value_StringValue{StringValue: "buildbucket_bucket:project/bucket"}},
   381  									{Kind: &structpb.Value_StringValue{StringValue: "buildbucket_build_id:1"}},
   382  									{Kind: &structpb.Value_StringValue{StringValue: "buildbucket_hostname:some unique host name"}},
   383  									{Kind: &structpb.Value_StringValue{StringValue: "buildbucket_template_canary:0"}},
   384  									{Kind: &structpb.Value_StringValue{StringValue: "luci_project:project"}},
   385  								},
   386  							},
   387  						},
   388  					},
   389  					"task_name": &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "bb-1-project/bucket"}},
   390  				},
   391  			})
   392  			So(req.BuildbucketHost, ShouldEqual, "some unique host name")
   393  			So(req.BuildId, ShouldEqual, "1")
   394  			So(req.Caches, ShouldResembleProto, []*pb.CacheEntry{
   395  				{
   396  					Name: "cache_name",
   397  					Path: "cache_value",
   398  				},
   399  				{
   400  					Name: "cipd_client_hash",
   401  					Path: "cipd_client",
   402  				},
   403  				{
   404  					Name: "cipd_cache_hash",
   405  					Path: "cipd_cache",
   406  				},
   407  			})
   408  			So(req.ExecutionTimeout, ShouldResembleProto, &durationpb.Duration{Seconds: 500})
   409  			So(req.GracePeriod, ShouldResembleProto, &durationpb.Duration{Seconds: 230})
   410  			So(req.Agent.Source["infra/tools/luci/bbagent/linux-amd64"], ShouldResembleProto, &pb.RunTaskRequest_AgentExecutable_AgentSource{
   411  				Sha256:    "this_is_a_sha_256_I_swear",
   412  				SizeBytes: 100,
   413  				Url:       "https://chrome-infra-packages.appspot.com/bootstrap/infra/tools/luci/bbagent/linux-amd64/+/latest",
   414  			})
   415  			So(req.Agent.Source["infra/tools/luci/bbagent/mac-amd64"], ShouldResembleProto, &pb.RunTaskRequest_AgentExecutable_AgentSource{
   416  				Sha256:    "this_is_a_sha_256_I_swear",
   417  				SizeBytes: 100,
   418  				Url:       "https://chrome-infra-packages.appspot.com/bootstrap/infra/tools/luci/bbagent/mac-amd64/+/latest",
   419  			})
   420  			So(req.AgentArgs, ShouldResemble, []string{
   421  				"-build-id", "1",
   422  				"-host", "some unique host name",
   423  				"-cache-base", "cache",
   424  				"-context-file", "${BUILDBUCKET_AGENT_CONTEXT_FILE}",
   425  			})
   426  			So(req.Dimensions, ShouldResembleProto, []*pb.RequestedDimension{
   427  				{
   428  					Key:   "dim_key_1",
   429  					Value: "dim_val_1",
   430  				},
   431  			})
   432  			So(req.StartDeadline.Seconds, ShouldEqual, 1677511893)
   433  			So(req.Experiments, ShouldResemble, []string{
   434  				"cow_eggs_experiment",
   435  				"are_cow_eggs_real_experiment",
   436  			})
   437  			So(req.PubsubTopic, ShouldEqual, "projects/app-id/topics/chromium-swarm-dev-backend")
   438  		})
   439  	})
   440  
   441  	Convey("RunTask", t, func(c C) {
   442  		ctl := gomock.NewController(t)
   443  		defer ctl.Finish()
   444  		mockTaskCreator := clients.NewMockTaskCreator(ctl)
   445  		now := testclock.TestRecentTimeUTC
   446  		ctx, _ := testclock.UseTime(context.Background(), now)
   447  		ctx = context.WithValue(ctx, clients.MockTaskCreatorKey, mockTaskCreator)
   448  		ctx = caching.WithEmptyProcessCache(ctx)
   449  		ctx = memory.UseWithAppID(ctx, "dev~app-id")
   450  		ctx = txndefer.FilterRDS(ctx)
   451  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   452  		datastore.GetTestable(ctx).AutoIndex(true)
   453  		datastore.GetTestable(ctx).Consistent(true)
   454  		ctx, _ = tq.TestingContext(ctx, nil)
   455  		store := &testsecrets.Store{
   456  			Secrets: map[string]secrets.Secret{
   457  				"key": {Active: []byte("stuff")},
   458  			},
   459  		}
   460  		ctx = secrets.Use(ctx, store)
   461  		ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx)
   462  		ctx = memlogger.Use(ctx)
   463  		ctx, sch := tq.TestingContext(txndefer.FilterRDS(ctx), nil)
   464  		logs := logging.Get(ctx).(*memlogger.MemLogger)
   465  
   466  		backendSetting := []*pb.BackendSetting{}
   467  		backendSetting = append(backendSetting, &pb.BackendSetting{
   468  			Target:   "fail_me",
   469  			Hostname: "hostname",
   470  		})
   471  		backendSetting = append(backendSetting, &pb.BackendSetting{
   472  			Target:   "swarming://chromium-swarm",
   473  			Hostname: "hostname2",
   474  			Mode: &pb.BackendSetting_FullMode_{
   475  				FullMode: &pb.BackendSetting_FullMode{
   476  					BuildSyncSetting: &pb.BackendSetting_BuildSyncSetting{
   477  						Shards:              5,
   478  						SyncIntervalSeconds: 300,
   479  					},
   480  				},
   481  			},
   482  			TaskCreatingTimeout: durationpb.New(8 * time.Minute),
   483  		})
   484  		backendSetting = append(backendSetting, &pb.BackendSetting{
   485  			Target:   "lite://foo-lite",
   486  			Hostname: "foo-hostname",
   487  			Mode: &pb.BackendSetting_LiteMode_{
   488  				LiteMode: &pb.BackendSetting_LiteMode{},
   489  			},
   490  		})
   491  		settingsCfg := &pb.SettingsCfg{Backends: backendSetting}
   492  		err := config.SetTestSettingsCfg(ctx, settingsCfg)
   493  		So(err, ShouldBeNil)
   494  		server := httptest.NewServer(describeBootstrapBundle(c, false))
   495  		defer server.Close()
   496  		client := &prpc.Client{
   497  			Host: strings.TrimPrefix(server.URL, "http://"),
   498  			Options: &prpc.Options{
   499  				Retry: func() retry.Iterator {
   500  					return &retry.Limited{
   501  						Retries: 3,
   502  						Delay:   0,
   503  					}
   504  				},
   505  				Insecure:  true,
   506  				UserAgent: "prpc-test",
   507  			},
   508  		}
   509  		ctx = context.WithValue(ctx, MockCipdClientKey{}, client)
   510  
   511  		build := &model.Build{
   512  			ID: 1,
   513  			Proto: &pb.Build{
   514  				Id: 1,
   515  				Builder: &pb.BuilderID{
   516  					Builder: "builder",
   517  					Bucket:  "bucket",
   518  					Project: "project",
   519  				},
   520  				CreateTime:       timestamppb.New(now.Add(-5 * time.Minute)),
   521  				ExecutionTimeout: &durationpb.Duration{Seconds: 500},
   522  				Input: &pb.Build_Input{
   523  					Experiments: []string{
   524  						"cow_eggs_experiment",
   525  						"are_cow_eggs_real_experiment",
   526  					},
   527  				},
   528  				GracePeriod: &durationpb.Duration{Seconds: 50},
   529  			},
   530  		}
   531  		key := datastore.KeyForObj(ctx, build)
   532  		infra := &model.BuildInfra{
   533  			Build: key,
   534  			Proto: &pb.BuildInfra{
   535  				Backend: &pb.BuildInfra_Backend{
   536  					Caches: []*pb.CacheEntry{
   537  						{
   538  							Name: "cache_name",
   539  							Path: "cache_value",
   540  						},
   541  					},
   542  					Config: &structpb.Struct{
   543  						Fields: map[string]*structpb.Value{
   544  							"priority": {
   545  								Kind: &structpb.Value_NumberValue{NumberValue: 32},
   546  							},
   547  							"bot_ping_tolerance": {
   548  								Kind: &structpb.Value_NumberValue{NumberValue: 2},
   549  							},
   550  						},
   551  					},
   552  					Task: &pb.Task{
   553  						Id: &pb.TaskID{
   554  							Id:     "",
   555  							Target: "swarming://chromium-swarm",
   556  						},
   557  					},
   558  					TaskDimensions: []*pb.RequestedDimension{
   559  						{
   560  							Key:   "dim_key_1",
   561  							Value: "dim_val_1",
   562  						},
   563  					},
   564  				},
   565  				Bbagent: &pb.BuildInfra_BBAgent{
   566  					CacheDir: "cache",
   567  				},
   568  				Buildbucket: &pb.BuildInfra_Buildbucket{
   569  					Hostname: "some unique host name",
   570  					Agent: &pb.BuildInfra_Buildbucket_Agent{
   571  						Source: &pb.BuildInfra_Buildbucket_Agent_Source{
   572  							DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
   573  								Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
   574  									Package: "infra/tools/luci/bbagent/${platform}",
   575  									Version: "latest",
   576  									Server:  "https://chrome-infra-packages.appspot.com",
   577  								},
   578  							},
   579  						},
   580  					},
   581  				},
   582  			},
   583  		}
   584  		bs := &model.BuildStatus{Build: datastore.KeyForObj(ctx, build)}
   585  		So(datastore.Put(ctx, build, infra, bs), ShouldBeNil)
   586  
   587  		Convey("ok", func() {
   588  			mockTaskCreator.EXPECT().RunTask(gomock.Any(), gomock.Any()).Return(&pb.RunTaskResponse{
   589  				Task: &pb.Task{
   590  					Id:       &pb.TaskID{Id: "abc123", Target: "swarming://chromium-swarm"},
   591  					Link:     "this_is_a_url_link",
   592  					UpdateId: 1,
   593  				},
   594  			}, nil)
   595  			err = CreateBackendTask(ctx, 1, "request_id")
   596  			So(err, ShouldBeNil)
   597  			eb := &model.Build{ID: build.ID}
   598  			expectedBuildInfra := &model.BuildInfra{Build: key}
   599  			So(datastore.Get(ctx, eb, expectedBuildInfra), ShouldBeNil)
   600  			updateTime := eb.Proto.UpdateTime.AsTime()
   601  			So(updateTime, ShouldEqual, now)
   602  			So(eb.BackendTarget, ShouldEqual, "swarming://chromium-swarm")
   603  			So(eb.BackendSyncInterval, ShouldEqual, time.Duration(300)*time.Second)
   604  			parts := strings.Split(eb.NextBackendSyncTime, "--")
   605  			So(parts, ShouldHaveLength, 4)
   606  			So(parts[0], ShouldEqual, eb.BackendTarget)
   607  			So(parts[1], ShouldEqual, "project")
   608  			shardID, err := strconv.Atoi(parts[2])
   609  			So(err, ShouldBeNil)
   610  			So(shardID >= 0, ShouldBeTrue)
   611  			So(shardID < 5, ShouldBeTrue)
   612  			So(parts[3], ShouldEqual, fmt.Sprint(updateTime.Round(time.Minute).Add(eb.BackendSyncInterval).Unix()))
   613  			So(expectedBuildInfra.Proto.Backend.Task, ShouldResembleProto, &pb.Task{
   614  				Id: &pb.TaskID{
   615  					Id:     "abc123",
   616  					Target: "swarming://chromium-swarm",
   617  				},
   618  				Link:     "this_is_a_url_link",
   619  				UpdateId: 1,
   620  			})
   621  			So(sch.Tasks(), ShouldBeEmpty)
   622  		})
   623  
   624  		Convey("fail", func() {
   625  			mockTaskCreator.EXPECT().RunTask(gomock.Any(), gomock.Any()).Return(nil, &googleapi.Error{Code: 400})
   626  			err = CreateBackendTask(ctx, 1, "request_id")
   627  			expectedBuild := &model.Build{ID: 1}
   628  			So(datastore.Get(ctx, expectedBuild), ShouldBeNil)
   629  			So(err, ShouldErrLike, "failed to create a backend task")
   630  			So(expectedBuild.Proto.Status, ShouldEqual, pb.Status_INFRA_FAILURE)
   631  			So(expectedBuild.Proto.SummaryMarkdown, ShouldContainSubstring, "Backend task creation failure.")
   632  		})
   633  
   634  		Convey("bail out if the build has a task associated", func() {
   635  			infra.Proto.Backend.Task.Id.Id = "task"
   636  			So(datastore.Put(ctx, infra), ShouldBeNil)
   637  			err = CreateBackendTask(ctx, 1, "request_id")
   638  			So(err, ShouldBeNil)
   639  			expectedBuildInfra := &model.BuildInfra{Build: key}
   640  			So(datastore.Get(ctx, expectedBuildInfra), ShouldBeNil)
   641  			So(expectedBuildInfra.Proto.Backend.Task.Id.Id, ShouldEqual, "task")
   642  			So(logs, memlogger.ShouldHaveLog,
   643  				logging.Info, "build 1 has associated with task")
   644  		})
   645  
   646  		Convey("give up after backend timeout", func() {
   647  			now = now.Add(9 * time.Minute)
   648  			ctx, _ = testclock.UseTime(ctx, now)
   649  			err = CreateBackendTask(ctx, 1, "request_id")
   650  			expectedBuild := &model.Build{ID: 1}
   651  			So(datastore.Get(ctx, expectedBuild), ShouldBeNil)
   652  			So(err, ShouldErrLike, "creating backend task for build 1 with requestID request_id has expired after 8m0s")
   653  			So(expectedBuild.Proto.Status, ShouldEqual, pb.Status_INFRA_FAILURE)
   654  			So(expectedBuild.Proto.SummaryMarkdown, ShouldContainSubstring, "Backend task creation failure.")
   655  		})
   656  
   657  		Convey("Lite backend", func() {
   658  			bkt := &model.Bucket{
   659  				ID:     "bucket",
   660  				Parent: model.ProjectKey(ctx, "project"),
   661  			}
   662  			bldr := &model.Builder{
   663  				ID:     "builder",
   664  				Parent: datastore.KeyForObj(ctx, bkt),
   665  				Config: &pb.BuilderConfig{
   666  					Name:                 "builder",
   667  					HeartbeatTimeoutSecs: 5,
   668  				},
   669  			}
   670  			build.Proto.SchedulingTimeout = durationpb.New(1 * time.Minute)
   671  			infra.Proto.Backend.Task.Id.Target = "lite://foo-lite"
   672  			So(datastore.Put(ctx, build, infra, bkt, bldr), ShouldBeNil)
   673  
   674  			mockTaskCreator.EXPECT().RunTask(gomock.Any(), gomock.Any()).Return(&pb.RunTaskResponse{
   675  				Task: &pb.Task{
   676  					Id:       &pb.TaskID{Id: "abc123", Target: "lite://foo-lite"},
   677  					Link:     "this_is_a_url_link",
   678  					UpdateId: 1,
   679  				},
   680  			}, nil)
   681  
   682  			Convey("ok", func() {
   683  				err = CreateBackendTask(ctx, 1, "request_id")
   684  				So(err, ShouldBeNil)
   685  				eb := &model.Build{ID: build.ID}
   686  				expectedBuildInfra := &model.BuildInfra{Build: datastore.KeyForObj(ctx, build)}
   687  				So(datastore.Get(ctx, eb, expectedBuildInfra), ShouldBeNil)
   688  				updateTime := eb.Proto.UpdateTime.AsTime()
   689  				So(updateTime, ShouldEqual, now)
   690  				So(expectedBuildInfra.Proto.Backend.Task, ShouldResembleProto, &pb.Task{
   691  					Id: &pb.TaskID{
   692  						Id:     "abc123",
   693  						Target: "lite://foo-lite",
   694  					},
   695  					Link:     "this_is_a_url_link",
   696  					UpdateId: 1,
   697  				})
   698  				tasks := sch.Tasks()
   699  				So(tasks, ShouldHaveLength, 1)
   700  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetBuildId(), ShouldEqual, build.ID)
   701  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetHeartbeatTimeout(), ShouldEqual, 5)
   702  				So(tasks[0].ETA, ShouldEqual, now.Add(5*time.Second))
   703  			})
   704  
   705  			Convey("SchedulingTimeout shorter than heartbeat timeout", func() {
   706  				bldr.Config.HeartbeatTimeoutSecs = 60
   707  				build.Proto.SchedulingTimeout = durationpb.New(10 * time.Second)
   708  				So(datastore.Put(ctx, build, bldr), ShouldBeNil)
   709  
   710  				err = CreateBackendTask(ctx, 1, "request_id")
   711  				So(err, ShouldBeNil)
   712  				tasks := sch.Tasks()
   713  				So(tasks, ShouldHaveLength, 1)
   714  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetBuildId(), ShouldEqual, build.ID)
   715  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetHeartbeatTimeout(), ShouldEqual, 60)
   716  				So(tasks[0].ETA, ShouldEqual, now.Add(10*time.Second))
   717  			})
   718  
   719  			Convey("no heartbeat_timeout_secs field set", func() {
   720  				bldr.Config.HeartbeatTimeoutSecs = 0
   721  				build.Proto.SchedulingTimeout = durationpb.New(10 * time.Second)
   722  				So(datastore.Put(ctx, build, bldr), ShouldBeNil)
   723  
   724  				err = CreateBackendTask(ctx, 1, "request_id")
   725  				So(err, ShouldBeNil)
   726  				tasks := sch.Tasks()
   727  				So(tasks, ShouldHaveLength, 1)
   728  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetBuildId(), ShouldEqual, build.ID)
   729  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetHeartbeatTimeout(), ShouldEqual, 0)
   730  				So(tasks[0].ETA, ShouldEqual, now.Add(10*time.Second))
   731  			})
   732  
   733  			Convey("builder not found", func() {
   734  				So(datastore.Delete(ctx, bldr), ShouldBeNil)
   735  				err = CreateBackendTask(ctx, 1, "request_id")
   736  				So(err, ShouldErrLike, "failed to fetch builder project/bucket/builder: datastore: no such entity")
   737  
   738  			})
   739  
   740  			Convey("in dynamic bucket", func() {
   741  				So(datastore.Delete(ctx, bldr, bkt), ShouldBeNil)
   742  				bkt.Proto = &pb.Bucket{
   743  					Name: "bucket",
   744  					DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{
   745  						Template: &pb.BuilderConfig{},
   746  					},
   747  				}
   748  				So(datastore.Put(ctx, bkt), ShouldBeNil)
   749  				err = CreateBackendTask(ctx, 1, "request_id")
   750  				So(err, ShouldBeNil)
   751  				tasks := sch.Tasks()
   752  				So(tasks, ShouldHaveLength, 1)
   753  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetBuildId(), ShouldEqual, build.ID)
   754  				So(tasks[0].Payload.(*taskdefs.CheckBuildLiveness).GetHeartbeatTimeout(), ShouldEqual, 0)
   755  				So(tasks[0].ETA, ShouldEqual, now.Add(1*time.Minute))
   756  			})
   757  		})
   758  	})
   759  }