go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/baselineupdater/baselineupdater.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 baselineupdater marks test variants from an invocation as submitted by
    16  // adding the test variants to the set of tests for its baseline identifier.
    17  package baselineupdater
    18  
    19  import (
    20  	"context"
    21  
    22  	"cloud.google.com/go/spanner"
    23  
    24  	"google.golang.org/grpc/status"
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/common/proto/mask"
    30  	"go.chromium.org/luci/common/retry/transient"
    31  
    32  	"go.chromium.org/luci/resultdb/internal/baselines"
    33  	btv "go.chromium.org/luci/resultdb/internal/baselines/testvariants"
    34  	"go.chromium.org/luci/resultdb/internal/invocations"
    35  	"go.chromium.org/luci/resultdb/internal/invocations/graph"
    36  	"go.chromium.org/luci/resultdb/internal/tasks/taskspb"
    37  
    38  	tr "go.chromium.org/luci/resultdb/internal/testresults"
    39  	pb "go.chromium.org/luci/resultdb/proto/v1"
    40  
    41  	"go.chromium.org/luci/server"
    42  	"go.chromium.org/luci/server/auth/realms"
    43  	"go.chromium.org/luci/server/span"
    44  	"go.chromium.org/luci/server/tq"
    45  )
    46  
    47  // BaselineUpdaterTasks describes how to route mark submitted tasks.
    48  var BaselineUpdaterTasks = tq.RegisterTaskClass(tq.TaskClass{
    49  	ID:            "update-baseline",
    50  	Prototype:     &taskspb.MarkInvocationSubmitted{},
    51  	Kind:          tq.Transactional,
    52  	Queue:         "baselineupdater",                 // use a dedicated queue
    53  	RoutingPrefix: "/internal/tasks/baselineupdater", // for routing to "baselineupdater" service
    54  })
    55  
    56  // TransactionLimit is set to 8000 because Cloud Spanner limits 40k mutations per transaction.
    57  // We have 5 columns to write to per row, and 40,000/5 = 8000.
    58  var TransactionLimit = 8000
    59  
    60  func Schedule(ctx context.Context, invID string) {
    61  	tq.MustAddTask(ctx, &tq.Task{
    62  		Payload: &taskspb.MarkInvocationSubmitted{InvocationId: invID},
    63  		Title:   invID,
    64  	})
    65  }
    66  
    67  // InitServer initializes a baselineupdator server.
    68  func InitServer(srv *server.Server) {
    69  	// init() below takes care of everything.
    70  }
    71  
    72  func init() {
    73  	BaselineUpdaterTasks.AttachHandler(func(ctx context.Context, msg proto.Message) error {
    74  		task := msg.(*taskspb.MarkInvocationSubmitted)
    75  		err := tryMarkInvocationSubmitted(ctx, invocations.ID(task.InvocationId))
    76  		if _, ok := status.FromError(errors.Unwrap(err)); ok {
    77  			// Spanner gRPC error.
    78  			return transient.Tag.Apply(err)
    79  		}
    80  		return err
    81  	})
    82  }
    83  
    84  // Marking an invocation is asynchronous. Invocations must be finalized prior
    85  // to it being marked submitted. Non-finalized invocations are marked as submitted
    86  // and will be scheduled by the finalizer.
    87  //
    88  // When an invocation is marked submitted, all test variants from that invocation
    89  // are added to the set of test variants for the invocation's baseline. In other
    90  // words, the set of tests expected to run for a baseline are updated with the
    91  // test variants from the provided invocation. Adding all test variants
    92  // from an invocation to its baseline are done recursively.
    93  //
    94  // For example, if invocation A for baseline "try:linux-rel" is finalized with
    95  // test A and B and "try:linux-rel" has A and C in its set of test variants,
    96  // this call would update the final set of test variants to (A, B and C).
    97  //
    98  // Marking an invocation submitted also updates the Baselines table with new
    99  // baselines.
   100  func tryMarkInvocationSubmitted(ctx context.Context, invID invocations.ID) error {
   101  	inv, err := invocations.Read(span.Single(ctx), invID)
   102  	if err != nil {
   103  		return errors.Annotate(err, "read invocation").Err()
   104  	}
   105  
   106  	if inv.BaselineId == "" {
   107  		// It's valid for a baseline to not be specified, so this workflow
   108  		// will terminate early.
   109  		return nil
   110  	}
   111  
   112  	if err := shouldMarkSubmitted(inv); err != nil {
   113  		return errors.Annotate(err, "mark invocation submitted").Err()
   114  	}
   115  
   116  	if err = ensureBaselineExists(ctx, inv); err != nil {
   117  		return errors.Annotate(err, "mark invocation submitted").Err()
   118  	}
   119  
   120  	return markInvocationSubmitted(ctx, inv)
   121  }
   122  
   123  // shouldMarkSubmitted returns an error if the invocation is ready to be marked
   124  // as submitted.
   125  func shouldMarkSubmitted(inv *pb.Invocation) error {
   126  	// all sub invocations should be finalized if the parent invocation is finalized.
   127  	if inv.State != pb.Invocation_FINALIZED {
   128  		return errors.Reason("the invocation is not yet finalized").Err()
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // markBaselineNew adds the baseline to the Baselines table if it's not present
   135  // in the BaselineTestVariants table. Baselines present will have the LastUpdated
   136  // time reset to the commit timestamp.
   137  func ensureBaselineExists(ctx context.Context, inv *pb.Invocation) error {
   138  	_, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
   139  		project, _ := realms.Split(inv.Realm)
   140  		baselineID := inv.BaselineId
   141  
   142  		_, err := baselines.Read(ctx, project, baselineID)
   143  		if err != nil {
   144  			if err == baselines.NotFound {
   145  				// if it isn't found, we can create the baseline in the table and terminate.
   146  				span.BufferWrite(ctx, baselines.Create(project, baselineID))
   147  				return nil
   148  			} else {
   149  				return errors.Annotate(err, "read baseline").Err()
   150  			}
   151  		}
   152  
   153  		// If the baseline exists, we'll update LastUpdatedTime so that it is not
   154  		// automatically ejected from the table.
   155  		span.BufferWrite(ctx, baselines.UpdateLastUpdatedTime(project, baselineID))
   156  		return nil
   157  	})
   158  	if err != nil {
   159  		return errors.Annotate(err, "ensure baseline").Err()
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  func markInvocationSubmitted(ctx context.Context, inv *pb.Invocation) error {
   166  	invID := invocations.MustParseName(inv.Name)
   167  	baselineID := inv.BaselineId
   168  	project, _ := realms.Split(inv.Realm)
   169  
   170  	masks := mask.MustFromReadMask(&pb.TestResult{},
   171  		"test_id",
   172  		"variant_hash",
   173  		"status",
   174  	)
   175  
   176  	rCtx, cancel := span.ReadOnlyTransaction(ctx)
   177  	defer cancel()
   178  
   179  	idSet := make([]invocations.ID, 0)
   180  	idSet = append(idSet, invocations.ID(invID))
   181  	invs, err := graph.Reachable(rCtx, invocations.NewIDSet(invID))
   182  	if err != nil {
   183  		return errors.Annotate(err, "discover reachable invocations").Err()
   184  	}
   185  	for invID, reachableInv := range invs.Invocations {
   186  		if !reachableInv.HasTestResults {
   187  			continue
   188  		}
   189  		idSet = append(idSet, invID)
   190  	}
   191  	q := &tr.Query{
   192  		Predicate:     &pb.TestResultPredicate{},
   193  		PageSize:      0,
   194  		InvocationIDs: invocations.NewIDSet(idSet...),
   195  		Mask:          masks,
   196  	}
   197  
   198  	// This will sequentially load results and process them when we reach mutation
   199  	// limits. This is not the quickest way to process the results and has room
   200  	// for optimization.
   201  	ms := make([]*spanner.Mutation, 0)
   202  	err = q.Run(rCtx, func(tr *pb.TestResult) error {
   203  		if tr.Status == pb.TestStatus_SKIP {
   204  			// We'll ignore SKIPPED from being BaselineTestVariants. This allows
   205  			// it to be verified for flakiness when it no longer becomes skipped.
   206  			logging.Debugf(ctx, "Skipped adding %s for baselineID %s", tr.TestId, baselineID)
   207  			return nil
   208  		}
   209  		ms = append(ms, btv.InsertOrUpdate(project, baselineID, tr.TestId, tr.VariantHash))
   210  
   211  		if len(ms) >= TransactionLimit {
   212  			_, err := span.ReadWriteTransaction(ctx, func(rwCtx context.Context) error {
   213  				span.BufferWrite(rwCtx, ms...)
   214  				return nil
   215  			})
   216  			if err != nil {
   217  				return errors.Annotate(err, "write baseline test variants").Err()
   218  			}
   219  			ms = make([]*spanner.Mutation, 0)
   220  		}
   221  		return nil
   222  	})
   223  	if err != nil {
   224  		return errors.Annotate(err, "query test variants").Err()
   225  	}
   226  
   227  	// Insert remaining test variants as a final write transaction.
   228  	if len(ms) > 0 {
   229  		_, err := span.ReadWriteTransaction(ctx, func(rwCtx context.Context) error {
   230  			span.BufferWrite(rwCtx, ms...)
   231  			return nil
   232  		})
   233  		if err != nil {
   234  			return errors.Annotate(err, "write baseline test variants").Err()
   235  		}
   236  	}
   237  
   238  	return nil
   239  }