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 }