go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/poller/notify.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 poller 16 17 import ( 18 "context" 19 "time" 20 21 "go.chromium.org/luci/common/errors" 22 "go.chromium.org/luci/common/logging" 23 gerritpb "go.chromium.org/luci/common/proto/gerrit" 24 "go.chromium.org/luci/common/retry/transient" 25 "go.chromium.org/luci/common/sync/parallel" 26 "go.chromium.org/luci/gae/service/datastore" 27 28 "go.chromium.org/luci/cv/internal/changelist" 29 "go.chromium.org/luci/cv/internal/common" 30 ) 31 32 // maxLoadCLBatchSize limits how many CL entities are loaded at once for 33 // notifying PM. 34 const maxLoadCLBatchSize = 100 35 36 func (p *Poller) notifyOnMatchedCLs(ctx context.Context, luciProject, host string, changes []*gerritpb.ChangeInfo, forceNotifyPM bool, requester changelist.UpdateCLTask_Requester) error { 37 if len(changes) == 0 { 38 return nil 39 } 40 // TODO(tandrii): optimize by checking if CV is interested in the 41 // (host,project,ref) of these changes from before triggering tasks. 42 logging.Debugf(ctx, "scheduling %d CLUpdate tasks (forceNotifyPM: %t)", len(changes), forceNotifyPM) 43 44 if forceNotifyPM { 45 changeNumbers := make([]int64, len(changes)) 46 for i, c := range changes { 47 changeNumbers[i] = c.GetNumber() 48 } 49 if err := p.notifyPMifKnown(ctx, luciProject, host, changeNumbers, maxLoadCLBatchSize); err != nil { 50 return err 51 } 52 } 53 54 errs := parallel.WorkPool(min(10, len(changes)), func(work chan<- func() error) { 55 for _, c := range changes { 56 payload := &changelist.UpdateCLTask{ 57 LuciProject: luciProject, 58 ExternalId: string(changelist.MustGobID(host, c.GetNumber())), 59 Hint: &changelist.UpdateCLTask_Hint{ExternalUpdateTime: c.GetUpdated()}, 60 Requester: requester, 61 } 62 work <- func() error { 63 return p.clUpdater.Schedule(ctx, payload) 64 } 65 } 66 }) 67 return common.MostSevereError(errs) 68 } 69 70 func (p *Poller) notifyOnUnmatchedCLs(ctx context.Context, luciProject, host string, changes []int64, requester changelist.UpdateCLTask_Requester) error { 71 if len(changes) == 0 { 72 return nil 73 } 74 logging.Debugf(ctx, "notifying CL Updater and PM on %d no longer matched CLs", len(changes)) 75 76 if err := p.notifyPMifKnown(ctx, luciProject, host, changes, maxLoadCLBatchSize); err != nil { 77 return err 78 } 79 errs := parallel.WorkPool(min(10, len(changes)), func(work chan<- func() error) { 80 for i, c := range changes { 81 payload := &changelist.UpdateCLTask{ 82 LuciProject: luciProject, 83 ExternalId: string(changelist.MustGobID(host, c)), 84 Requester: requester, 85 } 86 // Distribute these tasks in time to avoid high peaks (e.g. see 87 // https://crbug.com/1211057). 88 delay := (fullPollInterval * time.Duration(i)) / time.Duration(len(changes)) 89 work <- func() error { 90 return p.clUpdater.ScheduleDelayed(ctx, payload, delay) 91 } 92 } 93 }) 94 return common.MostSevereError(errs) 95 } 96 97 // notifyPMifKnown notifies PM to update its CLs for each Gerrit Change 98 // with existing CL entity. 99 // 100 // For Gerrit Changes without a CL entity, either: 101 // - the Gerrit CL Updater will create it in the future and hence also notify 102 // the PM; 103 // - or if the Gerrit CL updater doesn't do it, then there is no point 104 // notifying the PM anyway. 105 // 106 // Obtains EVersion of each CL before notify a PM. Unfortunately, this loads a 107 // lot of information we don't need, such as Snapshot. So, load CLs in batches 108 // to avoid large memory footprint. 109 // 110 // In practice, most of these CLs would be already dscache-ed, so loading them 111 // is fast. 112 func (p *Poller) notifyPMifKnown(ctx context.Context, luciProject, host string, changes []int64, maxBatchSize int) error { 113 eids := make([]changelist.ExternalID, len(changes)) 114 for i, c := range changes { 115 eids[i] = changelist.MustGobID(host, c) 116 } 117 clids, err := changelist.Lookup(ctx, eids) 118 if err != nil { 119 return err 120 } 121 122 cls := make([]*changelist.CL, 0, maxBatchSize) 123 flush := func() error { 124 if err := datastore.Get(ctx, cls); err != nil { 125 return errors.Annotate(common.MostSevereError(err), "failed to load CLs").Tag(transient.Tag).Err() 126 } 127 return p.pm.NotifyCLsUpdated(ctx, luciProject, changelist.ToUpdatedEvents(cls...)) 128 } 129 130 for _, clid := range clids { 131 switch l := len(cls); { 132 case clid == 0: 133 case l == maxBatchSize: 134 if err := flush(); err != nil { 135 return err 136 } 137 cls = cls[:0] 138 default: 139 cls = append(cls, &changelist.CL{ID: clid}) 140 } 141 } 142 if len(cls) > 0 { 143 return flush() 144 } 145 return nil 146 }