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 }