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  }