go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/bq/export_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 bq
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"testing"
    22  	"time"
    23  
    24  	"google.golang.org/protobuf/types/known/durationpb"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/common/clock/testclock"
    28  	"go.chromium.org/luci/gae/impl/memory"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  	"go.chromium.org/luci/server/tq"
    31  	"go.chromium.org/luci/server/tq/tqtesting"
    32  
    33  	"go.chromium.org/luci/swarming/server/bq/taskspb"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  	. "go.chromium.org/luci/common/testing/assertions"
    37  )
    38  
    39  func init() {
    40  	RegisterTQTasks()
    41  }
    42  
    43  func TestExportStateCleanup(t *testing.T) {
    44  	t.Parallel()
    45  
    46  	Convey("With mocks", t, func() {
    47  		setup := func() (context.Context, testclock.TestClock, *tqtesting.Scheduler) {
    48  			ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
    49  			ctx = memory.Use(ctx)
    50  			ctx, skdr := tq.TestingContext(ctx, nil)
    51  			return ctx, tc, skdr
    52  		}
    53  		Convey("cron deletes age(exportState.Created) >= 24hrs", func() {
    54  			const n = 200
    55  			ctx, tc, _ := setup()
    56  			datastore.GetTestable(ctx).AutoIndex(true)
    57  			datastore.GetTestable(ctx).Consistent(true)
    58  			now := tc.Now()
    59  			newest := now.Add(-2 * time.Hour)
    60  			oldest := now.Add(-48 * time.Hour)
    61  			ents := make([]*ExportState, 0, n*2)
    62  			for i := 0; i < n; i++ {
    63  				oldest = oldest.Add(-exportDuration)
    64  				ents = append(ents, &ExportState{
    65  					Key: exportStateKey(ctx, &taskspb.CreateExportTask{
    66  						Start:        timestamppb.New(oldest),
    67  						Duration:     durationpb.New(exportDuration),
    68  						CloudProject: "foo",
    69  						Dataset:      "bar",
    70  						TableName:    TaskRequests,
    71  					}),
    72  					CreatedAt: oldest,
    73  				})
    74  
    75  				newest = newest.Add(exportDuration)
    76  				ents = append(ents, &ExportState{
    77  					Key: exportStateKey(ctx, &taskspb.CreateExportTask{
    78  						Start:        timestamppb.New(newest),
    79  						Duration:     durationpb.New(exportDuration),
    80  						CloudProject: "foo",
    81  						Dataset:      "bar",
    82  						TableName:    TaskRequests,
    83  					}),
    84  					CreatedAt: newest,
    85  				})
    86  			}
    87  			So(datastore.Put(ctx, ents), ShouldBeNil)
    88  			So(CleanupExportState(ctx), ShouldBeNil)
    89  			q := datastore.NewQuery(exportStateKind)
    90  			count := 0
    91  			cu := now.Add(-maxExportStateAge)
    92  			err := datastore.Run(ctx, q, func(e *ExportState) error {
    93  				if e.CreatedAt.After(cu) {
    94  					count += 1
    95  					return nil
    96  				}
    97  				return fmt.Errorf("Too recent: %+v, co: %s, now: %s", e, cu, now)
    98  			})
    99  			So(err, ShouldBeNil)
   100  			So(count, ShouldEqual, n)
   101  		})
   102  	})
   103  }
   104  
   105  func TestCreateExportTask(t *testing.T) {
   106  	t.Parallel()
   107  
   108  	// TODO(jonahhooper) Add custom error handler to test failure to schedule
   109  	// a task.
   110  	// See: https://crrev.com/c/5054492/11..14/swarming/server/bq/export.go#b97
   111  	Convey("With mocks", t, func() {
   112  		setup := func() (context.Context, testclock.TestClock, *tqtesting.Scheduler) {
   113  			ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
   114  			ctx = memory.Use(ctx)
   115  			ctx, skdr := tq.TestingContext(ctx, nil)
   116  			return ctx, tc, skdr
   117  		}
   118  		Convey("Creates 4 ExportTasks", func() {
   119  			ctx, tc, skdr := setup()
   120  			cutoff := tc.Now().UTC().Add(-latestAge)
   121  			start := cutoff.Add(-time.Minute)
   122  			// It appears that datastore testing implementation strips away
   123  			// nanosecond time precision.
   124  			// Truncate by microsecond matches this behaviour in UTs
   125  			start = start.Truncate(time.Microsecond)
   126  			state := ExportSchedule{
   127  				Key:        exportScheduleKey(ctx, TaskRequests),
   128  				NextExport: start,
   129  			}
   130  			err := datastore.Put(ctx, &state)
   131  			So(err, ShouldBeNil)
   132  			err = ScheduleExportTasks(ctx, "foo", "bar", TaskRequests)
   133  			So(err, ShouldBeNil)
   134  			expected := []*taskspb.CreateExportTask{
   135  				{
   136  					CloudProject: "foo",
   137  					Dataset:      "bar",
   138  					TableName:    TaskRequests,
   139  					Start:        timestamppb.New(start),
   140  					Duration:     durationpb.New(exportDuration),
   141  				},
   142  				{
   143  					CloudProject: "foo",
   144  					Dataset:      "bar",
   145  					TableName:    TaskRequests,
   146  					Start:        timestamppb.New(start.Add(1 * exportDuration)),
   147  					Duration:     durationpb.New(exportDuration),
   148  				},
   149  				{
   150  					CloudProject: "foo",
   151  					Dataset:      "bar",
   152  					TableName:    TaskRequests,
   153  					Start:        timestamppb.New(start.Add(2 * exportDuration)),
   154  					Duration:     durationpb.New(exportDuration),
   155  				},
   156  				{
   157  					CloudProject: "foo",
   158  					Dataset:      "bar",
   159  					TableName:    TaskRequests,
   160  					Start:        timestamppb.New(start.Add(3 * exportDuration)),
   161  					Duration:     durationpb.New(exportDuration),
   162  				},
   163  			}
   164  			So(skdr.Tasks(), ShouldHaveLength, len(expected))
   165  			payloads := make([]*taskspb.CreateExportTask, len(expected))
   166  			for idx, tsk := range skdr.Tasks().Payloads() {
   167  				payloads[idx] = tsk.(*taskspb.CreateExportTask)
   168  			}
   169  			// We don't care about order and the test scheduler doesn't appear
   170  			// to return results in order all the time so sort results by time
   171  			sort.Slice(payloads, func(i, j int) bool {
   172  				return payloads[i].Start.AsTime().Before(payloads[j].Start.AsTime())
   173  			})
   174  			So(payloads, ShouldResembleProto, expected)
   175  			err = datastore.Get(ctx, &state)
   176  			So(err, ShouldBeNil)
   177  			So(state.NextExport, ShouldEqual, start.Add(4*exportDuration))
   178  		})
   179  		Convey("Creates 0 export tasks if ExportSchedule doesn't exist", func() {
   180  			ctx, tc, skdr := setup()
   181  			err := ScheduleExportTasks(ctx, "foo", "bar", TaskRequests)
   182  			So(err, ShouldBeNil)
   183  			So(skdr.Tasks(), ShouldBeEmpty)
   184  			state := ExportSchedule{Key: exportScheduleKey(ctx, TaskRequests)}
   185  			err = datastore.Get(ctx, &state)
   186  			So(err, ShouldBeNil)
   187  			So(state.NextExport, ShouldEqual, tc.Now().Truncate(time.Minute))
   188  		})
   189  		Convey("We cannot create more than 20 tasks at a time", func() {
   190  			ctx, tc, skdr := setup()
   191  			start := tc.Now().Add(-(2 + 10) * time.Minute).Truncate(time.Minute)
   192  			state := ExportSchedule{
   193  				Key:        exportScheduleKey(ctx, TaskRequests),
   194  				NextExport: start,
   195  			}
   196  			So(datastore.Put(ctx, &state), ShouldBeNil)
   197  			err := ScheduleExportTasks(ctx, "foo", "bar", TaskRequests)
   198  			So(err, ShouldBeNil)
   199  			So(skdr.Tasks(), ShouldHaveLength, 20)
   200  		})
   201  		Convey("We cannot schedule an exportTask in the future", func() {
   202  			ctx, tc, skdr := setup()
   203  			start := tc.Now().Add(5 * time.Minute)
   204  			state := ExportSchedule{
   205  				Key:        exportScheduleKey(ctx, TaskRequests),
   206  				NextExport: start,
   207  			}
   208  			So(datastore.Put(ctx, &state), ShouldBeNil)
   209  			err := ScheduleExportTasks(ctx, "foo", "bar", TaskRequests)
   210  			So(err, ShouldBeNil)
   211  			So(skdr.Tasks(), ShouldHaveLength, 0)
   212  		})
   213  	})
   214  }