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 }