go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/listener/gerrit.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 listener
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"cloud.google.com/go/pubsub"
    24  	"google.golang.org/protobuf/encoding/protojson"
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    30  	"go.chromium.org/luci/common/retry/transient"
    31  
    32  	"go.chromium.org/luci/cv/internal/changelist"
    33  	listenerpb "go.chromium.org/luci/cv/settings/listener"
    34  )
    35  
    36  // gerritProcessor implements processor interface for Gerrit subscription.
    37  type gerritProcessor struct {
    38  	sch       scheduler
    39  	host      string
    40  	prjFinder *projectFinder
    41  	msgFormat listenerpb.Settings_GerritSubscription_MessageFormat
    42  }
    43  
    44  // process processes a given Gerrit pubsub message and schedules UpdateCLTask(s)
    45  // for all the LUCI projects watching the Gerrit repo.
    46  func (p *gerritProcessor) process(ctx context.Context, m *pubsub.Message) error {
    47  	if len(m.Data) == 0 {
    48  		return nil
    49  	}
    50  	msg := &gerritpb.SourceRepoEvent{}
    51  	switch p.msgFormat {
    52  	case listenerpb.Settings_GerritSubscription_MESSAGE_FORMAT_UNSPECIFIED:
    53  		// Validation shouldn't allow this config.
    54  		panic(fmt.Errorf("impossible; MESSAGE_FORMAT_UNSPECIFIED"))
    55  	case listenerpb.Settings_GerritSubscription_JSON:
    56  		if err := protojson.Unmarshal(m.Data, msg); err != nil {
    57  			return errors.Annotate(err, "protojson.Unmarshal").Err()
    58  		}
    59  	case listenerpb.Settings_GerritSubscription_PROTO_BINARY:
    60  		if err := proto.Unmarshal(m.Data, msg); err != nil {
    61  			return errors.Annotate(err, "proto.Unmarshal").Err()
    62  		}
    63  	default:
    64  		// This must be a bug. A unit test should prevent this from happening.
    65  		panic(fmt.Errorf("impossible; missing an enum for GerritSubscription.MessageFormat"))
    66  	}
    67  
    68  	var repo string
    69  	switch chunks := strings.SplitN(msg.Name, "/", 4); {
    70  	case len(chunks) != 4, chunks[0] != "projects", chunks[2] != "repos":
    71  		// This is the format of Gerrit pubsub payload. If the format unmatches,
    72  		// it's likely a bug in CV or Gerrit.
    73  		return errors.Reason("invalid SourceRepoEvent name: %q", msg.Name).Err()
    74  	default:
    75  		repo = chunks[3]
    76  	}
    77  
    78  	// If no project is watching the repo, don't bother parsing the payload.
    79  	prjs, err := p.prjFinder.lookup(ctx, p.host, repo)
    80  	switch {
    81  	case err != nil:
    82  		return errors.Annotate(err, "projectFinder.lookup").Err()
    83  	case len(prjs) == 0:
    84  		return nil
    85  	}
    86  
    87  	// MetaRevIDs by ExternalIDs.
    88  	var eidToMetaRevID map[string]string
    89  	if e := msg.GetRefUpdateEvent(); e != nil {
    90  		eidToMetaRevID = make(map[string]string, len(e.GetRefUpdates()))
    91  		for ref, ev := range e.GetRefUpdates() {
    92  			// CV is only interested in CL update events, of which ref name
    93  			// ends with "/meta" in the following format.
    94  			// : "refs/changes/<val>/<change_num>/meta"
    95  			chunks := strings.SplitN(ref, "/", 5)
    96  			switch {
    97  			case len(chunks) != 5,
    98  				chunks[0] != "refs",
    99  				chunks[1] != "changes",
   100  				chunks[4] != "meta":
   101  				continue
   102  			}
   103  			change, err := strconv.ParseInt(chunks[3], 10, 63)
   104  			if err != nil {
   105  				// Must be a bug either in Gerrit or CV.
   106  				return errors.Annotate(err, "invalid change num (%s): %s", chunks[3], msg).Err()
   107  			}
   108  			eid, err := changelist.GobID(p.host, change)
   109  			if err != nil {
   110  				return errors.Annotate(err, "changelist.GobID").Err()
   111  			}
   112  
   113  			switch prev, exist := eidToMetaRevID[string(eid)]; {
   114  			case exist && prev != ev.NewId:
   115  				// RefUpdateEvent is a map type. Therefore, a single pubsub
   116  				// message can have at most one update event for each of the CLs
   117  				// listed.
   118  				//
   119  				// If a duplicate ExternalID with different RevID is found,
   120  				// there is a bug in CV or Gerrit.
   121  				return errors.Reason("found multiple meta-rev-ids (%q, %q) for %q: %s",
   122  					prev, ev.NewId, eid, msg).Err()
   123  			case exist && prev == ev.NewId:
   124  				// Still strange, but ok.
   125  				logging.Warningf(ctx, "duplicate update events found for %q: %s", eid, msg)
   126  			case !exist:
   127  				eidToMetaRevID[string(eid)] = ev.NewId
   128  			}
   129  		}
   130  	}
   131  
   132  	for eid, meta := range eidToMetaRevID {
   133  		for _, prj := range prjs {
   134  			task := &changelist.UpdateCLTask{
   135  				LuciProject: prj,
   136  				ExternalId:  eid,
   137  				Requester:   changelist.UpdateCLTask_PUBSUB_POLL,
   138  				Hint:        &changelist.UpdateCLTask_Hint{MetaRevId: meta},
   139  			}
   140  			if err := p.sch.Schedule(ctx, task); err != nil {
   141  				return errors.Annotate(err, "Schedule").Tag(transient.Tag).Err()
   142  			}
   143  		}
   144  	}
   145  	return nil
   146  }
   147  
   148  func newGerritSubscriber(c *pubsub.Client, sch scheduler, prjFinder *projectFinder, settings *listenerpb.Settings_GerritSubscription) *subscriber {
   149  	subID := settings.GetSubscriptionId()
   150  	if subID == "" {
   151  		subID = settings.GetHost()
   152  	}
   153  	sber := &subscriber{
   154  		sub: c.Subscription(subID),
   155  		proc: &gerritProcessor{
   156  			sch:       sch,
   157  			host:      settings.GetHost(),
   158  			prjFinder: prjFinder,
   159  			msgFormat: settings.GetMessageFormat(),
   160  		},
   161  	}
   162  	sber.sub.ReceiveSettings.NumGoroutines = defaultNumGoroutines
   163  	sber.sub.ReceiveSettings.MaxOutstandingMessages = defaultMaxOutstandingMessages
   164  	if val := settings.GetReceiveSettings().GetNumGoroutines(); val != 0 {
   165  		sber.sub.ReceiveSettings.NumGoroutines = int(val)
   166  	}
   167  	if val := settings.GetReceiveSettings().GetMaxOutstandingMessages(); val != 0 {
   168  		sber.sub.ReceiveSettings.MaxOutstandingMessages = int(val)
   169  	}
   170  	return sber
   171  }
   172  
   173  // sameGerritSubscriberSettings returns true if a given GerritSubscriber is
   174  // configured with given settings.
   175  func sameGerritSubscriberSettings(ctx context.Context, sber *subscriber, settings *listenerpb.Settings_GerritSubscription) (isSame bool) {
   176  	intendedSubID := settings.GetSubscriptionId()
   177  	if intendedSubID == "" {
   178  		intendedSubID = settings.GetHost()
   179  	}
   180  
   181  	ctx = logging.SetField(ctx, "subscriptionID", sber.sub.ID())
   182  	switch proc := sber.proc.(*gerritProcessor); {
   183  	case proc.host != settings.GetHost():
   184  		// This is suspicious enough to warn
   185  		logging.Warningf(ctx, "sameGerritSubscriberSettings: hostname changed from %q to %q",
   186  			proc.host, settings.GetHost())
   187  	case sber.sub.ID() != intendedSubID:
   188  		// Same
   189  		logging.Warningf(ctx, "sameGerritSubscriberSettings: subscription ID changed from %q to %q",
   190  			sber.sub.ID(), intendedSubID)
   191  	case !sber.sameReceiveSettings(ctx, settings.GetReceiveSettings()):
   192  	default:
   193  		isSame = true
   194  	}
   195  	return
   196  }