go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/listener/subscriber.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  	"reflect"
    20  	"sync"
    21  
    22  	"cloud.google.com/go/pubsub"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/logging"
    26  	"go.chromium.org/luci/common/retry/transient"
    27  	listenerpb "go.chromium.org/luci/cv/settings/listener"
    28  )
    29  
    30  const (
    31  	defaultNumGoroutines          = 10
    32  	defaultMaxOutstandingMessages = 1000
    33  )
    34  
    35  type processor interface {
    36  	// process processes a given pubsub message.
    37  	process(context.Context, *pubsub.Message) error
    38  }
    39  
    40  // subscriber receives and processes messages from a given subscription.
    41  type subscriber struct {
    42  	sub *pubsub.Subscription
    43  	// The message processor
    44  	proc processor
    45  
    46  	// protect cancelFunc and done
    47  	mu sync.Mutex
    48  	// nil before start
    49  	cancelFunc context.CancelFunc
    50  	// nil before start
    51  	done chan struct{}
    52  }
    53  
    54  // start starts a goroutine to receive and process messages from
    55  // the subscription continuously.
    56  //
    57  // The goroutine stops in any of the following occurrences.
    58  // - the context, passed to start, is done
    59  // - stop() is called
    60  //
    61  // Cannot be called while the subscriber is running.
    62  func (s *subscriber) start(ctx context.Context) error {
    63  	s.mu.Lock()
    64  	defer s.mu.Unlock()
    65  	if s.done != nil {
    66  		select {
    67  		case <-s.done:
    68  		default:
    69  			return errors.Reason("cannot start again, while the subscriber is running").Err()
    70  		}
    71  	}
    72  	switch ex, err := s.sub.Exists(ctx); {
    73  	case err != nil:
    74  		return errors.Annotate(err, "pubsub.Exists(%s)", s.sub.ID()).Err()
    75  	case !ex:
    76  		return errors.Reason("subscription %q doesn't exist", s.sub.ID()).Err()
    77  	}
    78  
    79  	ctx = logging.SetField(ctx, "subscriptionID", s.sub.ID())
    80  	subctx, cancel := context.WithCancel(ctx)
    81  	s.cancelFunc = cancel
    82  	s.done = make(chan struct{})
    83  	ch := make(chan struct{})
    84  
    85  	var procName string
    86  	switch t := reflect.TypeOf(s.proc); {
    87  	case t.Kind() == reflect.Ptr:
    88  		procName = t.Elem().Name()
    89  	default:
    90  		procName = t.Name()
    91  	}
    92  
    93  	go func() {
    94  		close(ch)
    95  		// cancel the context on exit.
    96  		defer cancel()
    97  		defer close(s.done)
    98  		logging.Infof(ctx, "subscriber.start: worker started")
    99  		err := s.sub.Receive(subctx, func(pubctx context.Context, m *pubsub.Message) {
   100  			if pubctx.Err() != nil {
   101  				logging.Warningf(subctx, "subscriber.process: %s", pubctx.Err())
   102  				m.Nack()
   103  				return
   104  			}
   105  
   106  			switch err := s.proc.process(pubctx, m); {
   107  			case err == nil:
   108  				m.Ack()
   109  			case pubctx.Err() != nil:
   110  				m.Nack()
   111  				logging.Warningf(subctx, "%s.process: %s", procName, err)
   112  			case transient.Tag.In(err):
   113  				m.Nack()
   114  				logging.Warningf(subctx, "%s.process: transient error %s", procName, err)
   115  			default:
   116  				// Ack the message, if there is a permanent error, as retry
   117  				// will unlikely fix the error.
   118  				//
   119  				// Full poll should rediscover the lost event.
   120  				m.Ack()
   121  				logging.Errorf(subctx, "%s.process: permanent error %s", procName, err)
   122  			}
   123  		})
   124  		// subctx may be no longer valid at this moment, use ctx for logging.
   125  		switch err {
   126  		case nil:
   127  			logging.Infof(ctx, "subscriber.start: worker exiting normally")
   128  		default:
   129  			logging.Errorf(ctx, "subscriber.start: worker exiting: %s", err)
   130  		}
   131  	}()
   132  
   133  	select {
   134  	case <-ch:
   135  	case <-ctx.Done():
   136  		// if the given context is done before the new goroutine starts,
   137  		// cancels the goroutine context so that it will be terminated
   138  		// after the start.
   139  		return ctx.Err()
   140  	}
   141  	return nil
   142  }
   143  
   144  func (s *subscriber) stop(ctx context.Context) {
   145  	s.mu.Lock()
   146  	ctx = logging.SetField(ctx, "subscriptionID", s.sub.ID())
   147  	logging.Infof(ctx, "subscriber.stop: requested")
   148  	defer s.mu.Unlock()
   149  	if s.cancelFunc != nil {
   150  		logging.Infof(ctx, "subscriber.stop: cancelling the context")
   151  		s.cancelFunc()
   152  		select {
   153  		case <-s.done:
   154  		case <-ctx.Done():
   155  			logging.Warningf(ctx, "subscriber.stop: stop context cancelled before worker ended")
   156  		}
   157  	}
   158  }
   159  
   160  // sameReceiveSettings returns true if the current receive settings are the same
   161  // as given ones.
   162  func (s *subscriber) sameReceiveSettings(ctx context.Context, in *listenerpb.Settings_ReceiveSettings) (isSame bool) {
   163  	ctx = logging.SetField(ctx, "subscriptionID", s.sub.ID())
   164  	intended := &listenerpb.Settings_ReceiveSettings{
   165  		NumGoroutines:          defaultNumGoroutines,
   166  		MaxOutstandingMessages: defaultMaxOutstandingMessages,
   167  	}
   168  
   169  	if val := in.GetNumGoroutines(); val > 0 {
   170  		intended.NumGoroutines = val
   171  	}
   172  	if val := in.GetMaxOutstandingMessages(); val > 0 {
   173  		intended.MaxOutstandingMessages = val
   174  	}
   175  
   176  	switch current := s.sub.ReceiveSettings; {
   177  	case current.NumGoroutines != int(intended.NumGoroutines):
   178  		logging.Infof(ctx, "sameReceiveSettings: NumGoroutines changed from %d to %d",
   179  			current.NumGoroutines, intended.NumGoroutines)
   180  	case current.MaxOutstandingMessages != int(intended.MaxOutstandingMessages):
   181  		logging.Infof(ctx, "sameReceiveSettings: MaxOutstandingMessages changed from %d to %d",
   182  			current.MaxOutstandingMessages, intended.MaxOutstandingMessages)
   183  	default:
   184  		isSame = true
   185  	}
   186  	return
   187  }
   188  
   189  func (s *subscriber) isStopped() bool {
   190  	s.mu.Lock()
   191  	defer s.mu.Unlock()
   192  	if s.done != nil {
   193  		select {
   194  		case <-s.done:
   195  		default:
   196  			return false
   197  		}
   198  	}
   199  	return true
   200  }