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 }