go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/sink/sink_server.go (about)

     1  // Copyright 2020 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 sink
    16  
    17  import (
    18  	"context"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"path"
    22  	"strings"
    23  	"sync"
    24  	"sync/atomic"
    25  
    26  	"github.com/golang/protobuf/proto"
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/metadata"
    29  	"google.golang.org/grpc/status"
    30  	"google.golang.org/protobuf/types/known/durationpb"
    31  	"google.golang.org/protobuf/types/known/emptypb"
    32  
    33  	"go.chromium.org/luci/common/clock"
    34  	"go.chromium.org/luci/common/data/rand/mathrand"
    35  	"go.chromium.org/luci/common/data/stringset"
    36  	"go.chromium.org/luci/common/errors"
    37  	"go.chromium.org/luci/common/logging"
    38  	"go.chromium.org/luci/common/sync/parallel"
    39  
    40  	"go.chromium.org/luci/resultdb/pbutil"
    41  	pb "go.chromium.org/luci/resultdb/proto/v1"
    42  	sinkpb "go.chromium.org/luci/resultdb/sink/proto/v1"
    43  )
    44  
    45  var zeroDuration = durationpb.New(0)
    46  
    47  // sinkServer implements sinkpb.SinkServer.
    48  type sinkServer struct {
    49  	cfg           ServerConfig
    50  	ac            *artifactChannel
    51  	tc            *testResultChannel
    52  	ec            *unexpectedPassChannel
    53  	resultIDBase  string
    54  	resultCounter uint32
    55  
    56  	// A set of invocation-level artifact IDs that have been uploaded.
    57  	// If an artifact is uploaded again, server should reject the request with
    58  	// error AlreadyExists.
    59  	invocationArtifactIDs stringset.Set
    60  	mu                    sync.Mutex
    61  }
    62  
    63  func newSinkServer(ctx context.Context, cfg ServerConfig) (sinkpb.SinkServer, error) {
    64  	// random bytes to generate a ResultID when ResultID unspecified in
    65  	// a TestResult.
    66  	bytes := make([]byte, 4)
    67  	if _, err := mathrand.Read(ctx, bytes); err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	ctx = metadata.AppendToOutgoingContext(ctx, pb.UpdateTokenMetadataKey, cfg.UpdateToken)
    72  	ss := &sinkServer{
    73  		cfg:                   cfg,
    74  		ac:                    newArtifactChannel(ctx, &cfg),
    75  		tc:                    newTestResultChannel(ctx, &cfg),
    76  		resultIDBase:          hex.EncodeToString(bytes),
    77  		invocationArtifactIDs: stringset.New(0),
    78  	}
    79  
    80  	if cfg.ExonerateUnexpectedPass {
    81  		ss.ec = newTestExonerationChannel(ctx, &cfg)
    82  	}
    83  
    84  	return &sinkpb.DecoratedSink{
    85  		Service: ss,
    86  		Prelude: authTokenPrelude(cfg.AuthToken),
    87  	}, nil
    88  }
    89  
    90  // closeSinkServer closes the dispatcher channels and blocks until they are fully drained,
    91  // or the context is cancelled.
    92  func closeSinkServer(ctx context.Context, s sinkpb.SinkServer) {
    93  	ss := s.(*sinkpb.DecoratedSink).Service.(*sinkServer)
    94  
    95  	logging.Infof(ctx, "SinkServer: draining TestResult channel started")
    96  	ss.tc.closeAndDrain(ctx)
    97  	logging.Infof(ctx, "SinkServer: draining TestResult channel ended")
    98  
    99  	logging.Infof(ctx, "SinkServer: draining Artifact channel started")
   100  	ss.ac.closeAndDrain(ctx)
   101  	logging.Infof(ctx, "SinkServer: draining Artifact channel ended")
   102  
   103  	if ss.ec != nil {
   104  		logging.Infof(ctx, "SinkServer: draining TestExoneration channel started")
   105  		ss.ec.closeAndDrain(ctx)
   106  		logging.Infof(ctx, "SinkServer: draining TestExoneration channel ended")
   107  	}
   108  }
   109  
   110  // authTokenValue returns the value of the Authorization HTTP header that all requests must
   111  // have.
   112  func authTokenValue(authToken string) string {
   113  	return fmt.Sprintf("%s %s", AuthTokenPrefix, authToken)
   114  }
   115  
   116  // authTokenValidator is a factory function generating a pRPC prelude that validates
   117  // a given HTTP request with the auth key.
   118  func authTokenPrelude(authToken string) func(context.Context, string, proto.Message) (context.Context, error) {
   119  	expected := authTokenValue(authToken)
   120  	missingKeyErr := status.Errorf(codes.Unauthenticated, "Authorization header is missing")
   121  
   122  	return func(ctx context.Context, _ string, _ proto.Message) (context.Context, error) {
   123  		md, ok := metadata.FromIncomingContext(ctx)
   124  		if !ok {
   125  			return nil, missingKeyErr
   126  		}
   127  		tks := md.Get(AuthTokenKey)
   128  		if len(tks) == 0 {
   129  			return nil, missingKeyErr
   130  		}
   131  		for _, tk := range tks {
   132  			if tk == expected {
   133  				return ctx, nil
   134  			}
   135  		}
   136  		return nil, status.Errorf(codes.PermissionDenied, "no valid auth_token found")
   137  	}
   138  }
   139  
   140  // ReportTestResults implement sinkpb.SinkServer.
   141  func (s *sinkServer) ReportTestResults(ctx context.Context, in *sinkpb.ReportTestResultsRequest) (*sinkpb.ReportTestResultsResponse, error) {
   142  	now := clock.Now(ctx).UTC()
   143  
   144  	// create a slice with a rough estimate.
   145  	uts := make([]*uploadTask, 0, len(in.TestResults)*4)
   146  	// Unexpected passed test results that need to be exonerated.
   147  	trsForExo := make([]*sinkpb.TestResult, 0, len(in.TestResults))
   148  	for _, tr := range in.TestResults {
   149  		tr.TestId = s.cfg.TestIDPrefix + tr.GetTestId()
   150  
   151  		// assign a random, unique ID if resultID omitted.
   152  		if tr.ResultId == "" {
   153  			tr.ResultId = fmt.Sprintf("%s-%.5d", s.resultIDBase, atomic.AddUint32(&s.resultCounter, 1))
   154  		}
   155  
   156  		if s.cfg.TestLocationBase != "" {
   157  			locFn := tr.GetTestMetadata().GetLocation().GetFileName()
   158  			// path.Join converts the leading double slashes to a single slash.
   159  			// Add a slash to keep the double slashes.
   160  			if locFn != "" && !strings.HasPrefix(locFn, "//") {
   161  				tr.TestMetadata.Location.FileName = "/" + path.Join(s.cfg.TestLocationBase, locFn)
   162  			}
   163  		}
   164  		// The system-clock of GCE machines may get updated by ntp while a test is running.
   165  		// It can possibly cause a negative duration produced, because most test harnesses
   166  		// use system-clock to calculate the run time of a test. For more info, visit
   167  		// crbug.com/1135892.
   168  		if duration := tr.GetDuration(); duration != nil && s.cfg.CoerceNegativeDuration {
   169  			// If a negative duration was reported, remove the duration.
   170  			if d := duration.AsDuration(); d < 0 {
   171  				logging.Warningf(ctx, "TestResult(%s) has a negative duration(%s); coercing it to 0", tr.TestId, d)
   172  				tr.Duration = zeroDuration
   173  			}
   174  		}
   175  
   176  		if err := validateTestResult(now, tr); err != nil {
   177  			return nil, status.Errorf(codes.InvalidArgument, "%s", err)
   178  		}
   179  
   180  		for id, a := range tr.GetArtifacts() {
   181  			updateArtifactContentType(a)
   182  			n := pbutil.TestResultArtifactName(s.cfg.invocationID, tr.TestId, tr.ResultId, id)
   183  			t, err := newUploadTask(n, a)
   184  
   185  			// newUploadTask can return an error if os.Stat() fails.
   186  			if err != nil {
   187  				// TODO(crbug.com/1124868) - once all test harnesses are fixed, return 4xx on
   188  				// newUploadTask failures instead of dropping the artifact silently.
   189  				logging.Warningf(ctx, "Dropping an artifact request; failed to create a new Uploadtask: %s",
   190  					errors.Annotate(err, "artifact %q: %s", id, err).Err())
   191  				continue
   192  			}
   193  			uts = append(uts, t)
   194  		}
   195  
   196  		if s.ec != nil && !tr.Expected && tr.Status == pb.TestStatus_PASS {
   197  			trsForExo = append(trsForExo, tr)
   198  		}
   199  	}
   200  
   201  	parallel.FanOutIn(func(work chan<- func() error) {
   202  		work <- func() error {
   203  			s.ac.schedule(uts...)
   204  			return nil
   205  		}
   206  		work <- func() error {
   207  			s.tc.schedule(in.TestResults...)
   208  			return nil
   209  		}
   210  		if s.ec != nil {
   211  			work <- func() error {
   212  				s.ec.schedule(trsForExo...)
   213  				return nil
   214  			}
   215  		}
   216  	})
   217  
   218  	// TODO(1017288) - set `TestResultNames` in the response
   219  	return &sinkpb.ReportTestResultsResponse{}, nil
   220  }
   221  
   222  // ReportInvocationLevelArtifacts implement sinkpb.SinkServer.
   223  func (s *sinkServer) ReportInvocationLevelArtifacts(ctx context.Context, in *sinkpb.ReportInvocationLevelArtifactsRequest) (*emptypb.Empty, error) {
   224  	uts := make([]*uploadTask, 0, len(in.Artifacts))
   225  	for id, a := range in.Artifacts {
   226  		updateArtifactContentType(a)
   227  		if err := validateArtifact(a); err != nil {
   228  			return nil, status.Errorf(codes.InvalidArgument, "bad request for artifact %q: %s", id, err)
   229  		}
   230  		t, err := newUploadTask(pbutil.InvocationArtifactName(s.cfg.invocationID, id), a)
   231  		// newUploadTask can return an error if os.Stat() fails.
   232  		if err != nil {
   233  			// TODO(crbug.com/1124868) - once all test harnesses are fixed, return 4xx on
   234  			// newUploadTask failures instead of dropping the artifact silently.
   235  			logging.Warningf(ctx, "Dropping an artifact request; failed to create a new Uploadtask: %s",
   236  				errors.Annotate(err, "artifact %q: %s", id, err).Err())
   237  			continue
   238  		}
   239  		s.mu.Lock()
   240  		if added := s.invocationArtifactIDs.Add(id); !added {
   241  			s.mu.Unlock()
   242  			return nil, status.Errorf(codes.AlreadyExists, "artifact %q has already been uploaded", id)
   243  		}
   244  		s.mu.Unlock()
   245  		uts = append(uts, t)
   246  	}
   247  	s.ac.schedule(uts...)
   248  
   249  	return &emptypb.Empty{}, nil
   250  }
   251  
   252  func updateArtifactContentType(a *sinkpb.Artifact) {
   253  	if a.GetFilePath() == "" || a.GetContentType() != "" {
   254  		return
   255  	}
   256  
   257  	switch path.Ext(a.GetFilePath()) {
   258  	case ".txt":
   259  		a.ContentType = "text/plain"
   260  	case ".html":
   261  		a.ContentType = "text/html"
   262  	case ".png":
   263  		a.ContentType = "image/png"
   264  	}
   265  }