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  }