go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/app/resultdb.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 app contains pub/sub handlers.
    16  package app
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"net/http"
    22  
    23  	"google.golang.org/protobuf/encoding/protojson"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/tsmon/field"
    27  	"go.chromium.org/luci/common/tsmon/metric"
    28  	rdbpb "go.chromium.org/luci/resultdb/proto/v1"
    29  	"go.chromium.org/luci/server/auth/realms"
    30  	"go.chromium.org/luci/server/router"
    31  
    32  	"go.chromium.org/luci/analysis/internal/ingestion/join"
    33  )
    34  
    35  var (
    36  	invocationsFinalizedCounter = metric.NewCounter(
    37  		"analysis/ingestion/pubsub/invocations_finalized",
    38  		"The number of finalized invocations received by LUCI Analysis from PubSub.",
    39  		nil,
    40  		// The LUCI Project.
    41  		field.String("project"),
    42  		// "success", "transient-failure", "permanent-failure" or "ignored".
    43  		field.String("status"))
    44  )
    45  
    46  type handleInvocationMethod func(ctx context.Context, notification *rdbpb.InvocationFinalizedNotification) (processed bool, err error)
    47  
    48  // InvocationFinalizedHandler accepts and processes ResultDB
    49  // Invocation Finalized Pub/Sub messages.
    50  type InvocationFinalizedHandler struct {
    51  	// The method to use to handle the deserialized invocation finalized
    52  	// notification. Used to allow the handler to be replaced for testing.
    53  	handleInvocation handleInvocationMethod
    54  }
    55  
    56  // NewInvocationFinalizedHandler initialises a new InvocationFinalizedHandler.
    57  func NewInvocationFinalizedHandler() *InvocationFinalizedHandler {
    58  	return &InvocationFinalizedHandler{
    59  		handleInvocation: join.JoinInvocation,
    60  	}
    61  }
    62  
    63  // Handle processes a ResultDB Invocation Finalized Pub/Sub message.
    64  func (h *InvocationFinalizedHandler) Handle(ctx *router.Context) {
    65  	status := "unknown"
    66  	project := "unknown"
    67  	defer func() {
    68  		// Closure for late binding.
    69  		invocationsFinalizedCounter.Add(ctx.Request.Context(), 1, project, status)
    70  	}()
    71  	project, processed, err := h.handleImpl(ctx.Request.Context(), ctx.Request)
    72  
    73  	switch {
    74  	case err != nil:
    75  		errors.Log(ctx.Request.Context(), errors.Annotate(err, "handling invocation finalized pubsub event").Err())
    76  		status = processErr(ctx, err)
    77  		return
    78  	case !processed:
    79  		status = "ignored"
    80  		// Use subtly different "success" response codes to surface in
    81  		// standard GAE logs whether an ingestion was ignored or not,
    82  		// while still acknowledging the pub/sub.
    83  		// See https://cloud.google.com/pubsub/docs/push#receiving_messages.
    84  		ctx.Writer.WriteHeader(http.StatusNoContent) // 204
    85  	default:
    86  		status = "success"
    87  		ctx.Writer.WriteHeader(http.StatusOK)
    88  	}
    89  }
    90  
    91  func (h *InvocationFinalizedHandler) handleImpl(ctx context.Context, request *http.Request) (project string, processed bool, err error) {
    92  	notification, err := extractNotification(request)
    93  	if err != nil {
    94  		return "unknown", false, errors.Annotate(err, "failed to extract invocation finalized notification").Err()
    95  	}
    96  
    97  	project, _ = realms.Split(notification.Realm)
    98  	processed, err = h.handleInvocation(ctx, notification)
    99  	if err != nil {
   100  		return project, false, errors.Annotate(err, "processing notification").Err()
   101  	}
   102  	return project, processed, nil
   103  }
   104  
   105  func extractNotification(r *http.Request) (*rdbpb.InvocationFinalizedNotification, error) {
   106  	var msg pubsubMessage
   107  	if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
   108  		return nil, errors.Annotate(err, "decoding pubsub message").Err()
   109  	}
   110  
   111  	var run rdbpb.InvocationFinalizedNotification
   112  	err := protojson.Unmarshal(msg.Message.Data, &run)
   113  	if err != nil {
   114  		return nil, errors.Annotate(err, "parsing pubsub message data").Err()
   115  	}
   116  	return &run, nil
   117  }