github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/lorry/engines/kafka/consumer.go (about) 1 /* 2 Copyright 2021 The Dapr Authors 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 http://www.apache.org/licenses/LICENSE-2.0 7 Unless required by applicable law or agreed to in writing, software 8 distributed under the License is distributed on an "AS IS" BASIS, 9 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 See the License for the specific language governing permissions and 11 limitations under the License. 12 */ 13 14 package kafka 15 16 import ( 17 "context" 18 "errors" 19 "fmt" 20 "sync" 21 "sync/atomic" 22 "time" 23 24 "github.com/1aal/kubeblocks/pkg/lorry/engines/kafka/thirdparty" 25 26 "github.com/Shopify/sarama" 27 "github.com/cenkalti/backoff/v4" 28 ) 29 30 type consumer struct { 31 k *Kafka 32 ready chan bool 33 running chan struct{} 34 stopped atomic.Bool 35 once sync.Once 36 } 37 38 func (consumer *consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { 39 b := consumer.k.backOffConfig.NewBackOffWithContext(session.Context()) 40 41 for { 42 select { 43 case message, ok := <-claim.Messages(): 44 if !ok { 45 return nil 46 } 47 48 if consumer.k.consumeRetryEnabled { 49 if err := thirdparty.NotifyRecover(func() error { 50 return consumer.doCallback(session, message) 51 }, b, func(err error, d time.Duration) { 52 consumer.k.logger.Error(err, fmt.Sprintf("Error processing Kafka message: %s/%d/%d [key=%s]. Retrying...", message.Topic, message.Partition, message.Offset, asBase64String(message.Key))) 53 }, func() { 54 consumer.k.logger.Info(fmt.Sprintf("Successfully processed Kafka message after it previously failed: %s/%d/%d [key=%s]", message.Topic, message.Partition, message.Offset, asBase64String(message.Key))) 55 }); err != nil { 56 consumer.k.logger.Error(err, fmt.Sprintf("Too many failed attempts at processing Kafka message: %s/%d/%d [key=%s]. ", message.Topic, message.Partition, message.Offset, asBase64String(message.Key))) 57 } 58 } else { 59 err := consumer.doCallback(session, message) 60 if err != nil { 61 consumer.k.logger.Error(err, "Error processing Kafka message: %s/%d/%d [key=%s].", message.Topic, message.Partition, message.Offset, asBase64String(message.Key)) 62 } 63 } 64 // Should return when `session.Context()` is done. 65 // If not, will raise `ErrRebalanceInProgress` or `read tcp <ip>:<port>: i/o timeout` when kafka rebalance. see: 66 // https://github.com/Shopify/sarama/issues/1192 67 case <-session.Context().Done(): 68 return nil 69 } 70 } 71 } 72 73 func (consumer *consumer) doCallback(session sarama.ConsumerGroupSession, message *sarama.ConsumerMessage) error { 74 consumer.k.logger.Info(fmt.Sprintf("Processing Kafka message: %s/%d/%d [key=%s]", message.Topic, message.Partition, message.Offset, asBase64String(message.Key))) 75 handlerConfig, err := consumer.k.GetTopicHandlerConfig(message.Topic) 76 if err != nil { 77 return err 78 } 79 if !handlerConfig.IsBulkSubscribe && handlerConfig.Handler == nil { 80 return errors.New("invalid handler config for subscribe call") 81 } 82 event := NewEvent{ 83 Topic: message.Topic, 84 Data: message.Value, 85 } 86 // This is true only when headers are set (Kafka > 0.11) 87 if len(message.Headers) > 0 { 88 event.Metadata = make(map[string]string, len(message.Headers)) 89 for _, header := range message.Headers { 90 event.Metadata[string(header.Key)] = string(header.Value) 91 } 92 } 93 err = handlerConfig.Handler(session.Context(), &event) 94 if err == nil { 95 session.MarkMessage(message, "") 96 } 97 return err 98 } 99 100 func (consumer *consumer) Cleanup(sarama.ConsumerGroupSession) error { 101 return nil 102 } 103 104 func (consumer *consumer) Setup(sarama.ConsumerGroupSession) error { 105 consumer.once.Do(func() { 106 close(consumer.ready) 107 }) 108 109 return nil 110 } 111 112 // AddTopicHandler adds a handler and configuration for a topic 113 func (k *Kafka) AddTopicHandler(topic string, handlerConfig SubscriptionHandlerConfig) { 114 k.subscribeLock.Lock() 115 k.subscribeTopics[topic] = handlerConfig 116 k.subscribeLock.Unlock() 117 } 118 119 // RemoveTopicHandler removes a topic handler 120 func (k *Kafka) RemoveTopicHandler(topic string) { 121 k.subscribeLock.Lock() 122 delete(k.subscribeTopics, topic) 123 k.subscribeLock.Unlock() 124 } 125 126 // GetTopicHandlerConfig returns the handlerConfig for a topic 127 func (k *Kafka) GetTopicHandlerConfig(topic string) (SubscriptionHandlerConfig, error) { 128 handlerConfig, ok := k.subscribeTopics[topic] 129 if ok && (!handlerConfig.IsBulkSubscribe && handlerConfig.Handler != nil) { 130 return handlerConfig, nil 131 } 132 return SubscriptionHandlerConfig{}, 133 fmt.Errorf("any handler for messages of topic %s not found", topic) 134 } 135 136 // Subscribe to topic in the Kafka cluster, in a background goroutine 137 func (k *Kafka) Subscribe(ctx context.Context) error { 138 if k.consumerGroup == "" { 139 return errors.New("kafka: consumerGroup must be set to subscribe") 140 } 141 142 k.subscribeLock.Lock() 143 defer k.subscribeLock.Unlock() 144 145 // Close resources and reset synchronization primitives 146 k.closeSubscriptionResources() 147 148 topics := k.subscribeTopics.TopicList() 149 if len(topics) == 0 { 150 // Nothing to subscribe to 151 return nil 152 } 153 154 cg, err := sarama.NewConsumerGroup(k.brokers, k.consumerGroup, k.config) 155 if err != nil { 156 return err 157 } 158 159 k.cg = cg 160 161 ready := make(chan bool) 162 k.consumer = consumer{ 163 k: k, 164 ready: ready, 165 running: make(chan struct{}), 166 } 167 168 go func() { 169 k.logger.Info("Subscribed and listening to topics", "topics", topics) 170 171 for { 172 // If the context was cancelled, as is the case when handling SIGINT and SIGTERM below, then this pops 173 // us out of the consume loop 174 if ctx.Err() != nil { 175 break 176 } 177 178 k.logger.Info("Starting loop to consume.") 179 180 // Consume the requested topics 181 bo := backoff.WithContext(backoff.NewConstantBackOff(k.consumeRetryInterval), ctx) 182 innerErr := thirdparty.NotifyRecover(func() error { 183 if ctxErr := ctx.Err(); ctxErr != nil { 184 return backoff.Permanent(ctxErr) 185 } 186 return k.cg.Consume(ctx, topics, &(k.consumer)) 187 }, bo, func(err error, t time.Duration) { 188 k.logger.Error(err, fmt.Sprintf("Error consuming %v. Retrying...", topics)) 189 }, func() { 190 k.logger.Info(fmt.Sprintf("Recovered consuming %v", topics)) 191 }) 192 if innerErr != nil && !errors.Is(innerErr, context.Canceled) { 193 k.logger.Error(innerErr, fmt.Sprintf("Permanent error consuming %v", topics)) 194 } 195 } 196 197 k.logger.Info(fmt.Sprintf("Closing ConsumerGroup for topics: %v", topics)) 198 err := k.cg.Close() 199 if err != nil { 200 k.logger.Error(err, "Error closing consumer group") 201 } 202 203 // Ensure running channel is only closed once. 204 if k.consumer.stopped.CompareAndSwap(false, true) { 205 close(k.consumer.running) 206 } 207 }() 208 209 <-ready 210 211 return nil 212 } 213 214 // Close down consumer group resources, refresh once. 215 func (k *Kafka) closeSubscriptionResources() { 216 if k.cg != nil { 217 err := k.cg.Close() 218 if err != nil { 219 k.logger.Error(err, "Error closing consumer group") 220 } 221 222 k.consumer.once.Do(func() { 223 // Wait for shutdown to be complete 224 <-k.consumer.running 225 close(k.consumer.ready) 226 k.consumer.once = sync.Once{} 227 }) 228 } 229 }