go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/listener/listener.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 "sync" 21 "time" 22 23 "cloud.google.com/go/pubsub" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/config" 30 31 "go.chromium.org/luci/cv/internal/changelist" 32 "go.chromium.org/luci/cv/internal/configs/srvcfg" 33 listenerpb "go.chromium.org/luci/cv/settings/listener" 34 ) 35 36 const ( 37 // reloadInterval defines how often Listener evaluates the latest copy of 38 // the subscription settings and adjusts subscribers as necessary. 39 reloadInterval = time.Minute 40 ) 41 42 // This interface encapsulate the communication with changelist.Updater. 43 type scheduler interface { 44 Schedule(context.Context, *changelist.UpdateCLTask) error 45 } 46 47 // Listener fetches and process messages from the subscriptions configured 48 // in the settings. 49 type Listener struct { 50 mu sync.Mutex 51 sbers map[string]*subscriber 52 sch scheduler 53 psClient *pubsub.Client 54 prjFinder *projectFinder 55 } 56 57 // NewListener constructs a Listener. 58 func NewListener(psClient *pubsub.Client, sch scheduler) *Listener { 59 return &Listener{ 60 sbers: make(map[string]*subscriber), 61 sch: sch, 62 psClient: psClient, 63 prjFinder: &projectFinder{}, 64 } 65 } 66 67 // Run continuously evaluates the listener settings and manages workers 68 // for each of the subscriptions configured. 69 func (l *Listener) Run(ctx context.Context) { 70 var prevHash string 71 72 for { 73 meta := &config.Meta{} 74 lcfg, err := srvcfg.GetListenerConfig(ctx, meta) 75 switch { 76 case err != nil: 77 logging.Errorf(ctx, "GetListenerConfig: %s", err) 78 case meta.ContentHash != prevHash: 79 // new config? 80 logging.Infof(ctx, "Listener.Run: new settings.cfg found: %s", meta.ContentHash) 81 if err := l.reload(ctx, lcfg); err != nil { 82 logging.Errorf(ctx, "Listener.reload: %s", err.Error()) 83 } 84 } 85 86 select { 87 case <-ctx.Done(): 88 logging.Infof(ctx, "Listener.Run: the context is done; exiting") 89 return 90 case <-clock.After(ctx, reloadInterval): 91 } 92 } 93 } 94 95 func (l *Listener) reload(ctx context.Context, s *listenerpb.Settings) error { 96 if err := l.prjFinder.reload(s); err != nil { 97 return errors.Annotate(err, "projectFinder.reload").Err() 98 } 99 if err := l.reloadSubscribers(ctx, s.GetGerritSubscriptions()); err != nil { 100 return errors.Annotate(err, "reloadSubscribers").Err() 101 } 102 return nil 103 } 104 105 // reloadSubscribers reloads the subscribers as configured in the settings. 106 // 107 // It will 108 // - start a subscriber for new subscription settings. 109 // - stop the subscriber for removed subscription settings. 110 // - restart the subscriber if it is found dead. 111 func (l *Listener) reloadSubscribers(ctx context.Context, settings []*listenerpb.Settings_GerritSubscription) error { 112 var wg sync.WaitGroup 113 activeHosts := stringset.New(len(settings)) 114 startErrs := errors.NewLazyMultiError(len(settings)) 115 l.mu.Lock() 116 defer l.mu.Unlock() 117 118 for i, setting := range settings { 119 i, setting := i, setting 120 host := setting.GetHost() 121 if !activeHosts.Add(host) { 122 panic(fmt.Errorf("duplicate host %q; there must be a bug in the cfg validation", host)) 123 } 124 125 switch sber, ok := l.sbers[host]; { 126 case ctx.Err() != nil: 127 return ctx.Err() 128 129 case !ok: 130 sber = newGerritSubscriber(l.psClient, l.sch, l.prjFinder, setting) 131 logging.Infof(ctx, "listener.reload: new host %q found; starting a subscriber", host) 132 if err := sber.start(ctx); err != nil { 133 startErrs.Assign(i, err) 134 } 135 l.sbers[host] = sber 136 137 // If the setting changed, stop the existing subscriber and 138 // start a new one. 139 case !sameGerritSubscriberSettings(ctx, sber, setting): 140 logging.Infof(ctx, "listener.reload: subscriber setting changed for host %q", host) 141 wg.Add(1) 142 newSber := newGerritSubscriber(l.psClient, l.sch, l.prjFinder, setting) 143 l.sbers[host] = newSber 144 145 go func() { 146 defer wg.Done() 147 sber.stop(ctx) 148 if err := newSber.start(ctx); err != nil { 149 startErrs.Assign(i, err) 150 } 151 }() 152 153 // did the subscriber stop or fail to start? 154 case sber.isStopped(): 155 logging.Warningf(ctx, "listener.reload: subscriber for host %q was found dead; restarting", host) 156 if err := sber.start(ctx); err != nil { 157 startErrs.Assign(i, err) 158 } 159 } 160 } 161 162 // stop the Gerrit subscribers for the removed Gerrit hosts. 163 for host, sber := range l.sbers { 164 if !activeHosts.Has(host) { 165 logging.Infof(ctx, "listener.reload: host %q was removed from the settings", host) 166 delete(l.sbers, host) 167 168 sber := sber 169 wg.Add(1) 170 go func() { 171 defer wg.Done() 172 sber.stop(ctx) 173 }() 174 } 175 } 176 wg.Wait() 177 return startErrs.Get() 178 } 179 180 // getSubscriber returns the subscriber for a given host. 181 // 182 // Returns nil if there isn't any. 183 func (l *Listener) getSubscriber(host string) *subscriber { 184 l.mu.Lock() 185 defer l.mu.Unlock() 186 return l.sbers[host] 187 }