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 }