github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/pubsub/subscriber/server.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package subscriber
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"reflect"
    23  	"strings"
    24  
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"cloud.google.com/go/pubsub"
    28  	"github.com/prometheus/client_golang/prometheus"
    29  	"golang.org/x/sync/errgroup"
    30  	"sigs.k8s.io/prow/pkg/config"
    31  )
    32  
    33  type configToWatch struct {
    34  	config.PubSubTriggers
    35  	config.PubsubSubscriptions
    36  }
    37  
    38  // PullServer listen to Pull Pub/Sub subscriptions and handle them.
    39  type PullServer struct {
    40  	Subscriber *Subscriber
    41  	Client     pubsubClientInterface
    42  }
    43  
    44  // NewPullServer creates a new PullServer
    45  func NewPullServer(s *Subscriber) *PullServer {
    46  	return &PullServer{
    47  		Subscriber: s,
    48  		Client:     &pubSubClient{},
    49  	}
    50  }
    51  
    52  // For testing
    53  type subscriptionInterface interface {
    54  	string() string
    55  	receive(ctx context.Context, f func(context.Context, messageInterface)) error
    56  }
    57  
    58  // pubsubClientInterface interfaces with Cloud Pub/Sub client for testing reason
    59  type pubsubClientInterface interface {
    60  	new(ctx context.Context, project string) (pubsubClientInterface, error)
    61  	subscription(id string, maxOutstandingMessages int) subscriptionInterface
    62  }
    63  
    64  // pubSubClient is used to interface with a new Cloud Pub/Sub Client
    65  type pubSubClient struct {
    66  	client *pubsub.Client
    67  }
    68  
    69  type pubSubSubscription struct {
    70  	sub *pubsub.Subscription
    71  }
    72  
    73  func (s *pubSubSubscription) string() string {
    74  	return s.sub.String()
    75  }
    76  
    77  func (s *pubSubSubscription) receive(ctx context.Context, f func(context.Context, messageInterface)) error {
    78  	g := func(ctx2 context.Context, msg2 *pubsub.Message) {
    79  		f(ctx2, &pubSubMessage{Message: *msg2})
    80  	}
    81  	return s.sub.Receive(ctx, g)
    82  }
    83  
    84  // New creates new Cloud Pub/Sub Client
    85  func (c *pubSubClient) new(ctx context.Context, project string) (pubsubClientInterface, error) {
    86  	client, err := pubsub.NewClient(ctx, project)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	c.client = client
    91  	return c, nil
    92  }
    93  
    94  // Subscription creates a reference to an existing subscription via the Cloud Pub/Sub Client.
    95  func (c *pubSubClient) subscription(id string, maxOutstandingMessages int) subscriptionInterface {
    96  	sub := c.client.Subscription(id)
    97  	sub.ReceiveSettings.MaxOutstandingMessages = maxOutstandingMessages
    98  	// Without this setting, a single Receiver can occupy more than the number of `MaxOutstandingMessages`,
    99  	// and other replicas of sub will have nothing to work on.
   100  	// cjwagner and chaodaiG understand it might not make much sense to set both MaxOutstandingMessages
   101  	// and Synchronous, nor did the GoDoc https://github.com/googleapis/google-cloud-go/blob/22ffc18e522c0f943db57f8c943e7356067bedfd/pubsub/subscription.go#L501
   102  	// agrees clearly with us, but trust us, both are required for making sure that every replica has something to do
   103  	sub.ReceiveSettings.Synchronous = true
   104  	return &pubSubSubscription{
   105  		sub: sub,
   106  	}
   107  }
   108  
   109  // handlePulls pull for Pub/Sub subscriptions and handle them.
   110  func (s *PullServer) handlePulls(ctx context.Context, projectSubscriptions config.PubSubTriggers) (*errgroup.Group, context.Context, error) {
   111  	// Since config might change we need be able to cancel the current run
   112  	errGroup, derivedCtx := errgroup.WithContext(ctx)
   113  	for _, topics := range projectSubscriptions {
   114  		project, subscriptions, allowedClusters := topics.Project, topics.Topics, topics.AllowedClusters
   115  		client, err := s.Client.new(ctx, project)
   116  		if err != nil {
   117  			return errGroup, derivedCtx, err
   118  		}
   119  		for _, subName := range subscriptions {
   120  			sub := client.subscription(subName, topics.MaxOutstandingMessages)
   121  			logger := logrus.WithFields(logrus.Fields{
   122  				"subscription": sub.string(),
   123  				"project":      project,
   124  			})
   125  			errGroup.Go(func() error {
   126  				logger.Info("Listening for subscription")
   127  				defer logger.Warn("Stopped Listening for subscription")
   128  				err := sub.receive(derivedCtx, func(ctx context.Context, msg messageInterface) {
   129  					if err = s.Subscriber.handleMessage(msg, sub.string(), allowedClusters); err != nil {
   130  						s.Subscriber.Metrics.ACKMessageCounter.With(prometheus.Labels{subscriptionLabel: sub.string()}).Inc()
   131  					} else {
   132  						s.Subscriber.Metrics.NACKMessageCounter.With(prometheus.Labels{subscriptionLabel: sub.string()}).Inc()
   133  					}
   134  					msg.ack()
   135  				})
   136  				if err != nil {
   137  					if errors.Is(derivedCtx.Err(), context.Canceled) {
   138  						logger.WithError(err).Debug("Exiting as context cancelled")
   139  						return nil
   140  					}
   141  					if strings.Contains(err.Error(), "code = PermissionDenied") {
   142  						logger.WithError(err).Warn("Seems like missing permission.")
   143  						return nil
   144  					}
   145  					logger.WithError(err).Error("Failed to listen for subscription")
   146  					return err
   147  				}
   148  				return nil
   149  			})
   150  		}
   151  	}
   152  	return errGroup, derivedCtx, nil
   153  }
   154  
   155  // Run will block listening to all subscriptions and return once the context is cancelled
   156  // or one of the subscription has a unrecoverable error.
   157  func (s *PullServer) Run(ctx context.Context) error {
   158  	configEvent := make(chan config.Delta, 2)
   159  	s.Subscriber.ConfigAgent.Subscribe(configEvent)
   160  
   161  	var err error
   162  	defer func() {
   163  		if err != nil {
   164  			logrus.WithError(ctx.Err()).Error("Pull server shutting down.")
   165  		}
   166  		logrus.Debug("Pull server shutting down.")
   167  	}()
   168  	currentConfig := configToWatch{
   169  		s.Subscriber.ConfigAgent.Config().PubSubTriggers,
   170  		s.Subscriber.ConfigAgent.Config().PubSubSubscriptions,
   171  	}
   172  	errGroup, derivedCtx, err := s.handlePulls(ctx, currentConfig.PubSubTriggers)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	for {
   178  		select {
   179  		// Parent context. Shutdown
   180  		case <-ctx.Done():
   181  			return nil
   182  		// Current thread context, it may be failing already
   183  		case <-derivedCtx.Done():
   184  			err = errGroup.Wait()
   185  			return err
   186  		// Checking for update config
   187  		case event := <-configEvent:
   188  			newConfig := configToWatch{
   189  				event.After.PubSubTriggers,
   190  				event.After.PubSubSubscriptions,
   191  			}
   192  			logrus.Info("Received new config")
   193  			if !reflect.DeepEqual(currentConfig, newConfig) {
   194  				logrus.Info("New config found, reloading pull Server")
   195  				// Making sure the current thread finishes before starting a new one.
   196  				errGroup.Wait()
   197  				// Starting a new thread with new config
   198  				errGroup, derivedCtx, err = s.handlePulls(ctx, newConfig.PubSubTriggers)
   199  				if err != nil {
   200  					return err
   201  				}
   202  				currentConfig = newConfig
   203  			}
   204  		}
   205  	}
   206  }