github.com/weedge/lib@v0.0.0-20230424045628-a36dcc1d90e4/client/mq/kafka/consumer/consumer_group.go (about)

     1  package consumer
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"github.com/weedge/lib/client/mq/kafka/auth"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/weedge/lib/container/set"
    11  	"github.com/weedge/lib/log"
    12  	"github.com/weedge/lib/runtimer"
    13  
    14  	"github.com/Shopify/sarama"
    15  )
    16  
    17  var consumerGroupNames *set.HashSet
    18  
    19  func init() {
    20  	consumerGroupNames = set.NewSet()
    21  }
    22  
    23  type IConsumerMsg interface {
    24  	Consumer(msg *sarama.ConsumerMessage) error
    25  }
    26  
    27  // Consumer represents a Sarama consumer group consumer
    28  type ConsumerGroup struct {
    29  	name      string // the same to groupId
    30  	ready     chan bool
    31  	config    *sarama.Config
    32  	topicList []string
    33  	client    sarama.ConsumerGroup
    34  	cancel    context.CancelFunc
    35  	wg        *sync.WaitGroup
    36  	msg       IConsumerMsg
    37  	msgMeta   string
    38  }
    39  
    40  // user just defined open consumer group option
    41  func NewConsumerGroup(name string, msg IConsumerMsg, authOpts []auth.Option, options ...Option) (consumer *ConsumerGroup, err error) {
    42  	consumer = &ConsumerGroup{name: name, wg: &sync.WaitGroup{}, msg: msg}
    43  
    44  	consumerOpts := getConsumerOptions(authOpts, options...)
    45  	log.Info(fmt.Sprintf("consumer options:%+v", consumerOpts))
    46  
    47  	consumer.ready = make(chan bool)
    48  	consumer.config = sarama.NewConfig()
    49  	consumer.config.Consumer.Return.Errors = true
    50  	consumer.topicList = consumerOpts.topicList
    51  	consumer.config.Version, err = sarama.ParseKafkaVersion(consumerOpts.version)
    52  	if err != nil {
    53  		return
    54  	}
    55  
    56  	switch consumerOpts.reBalanceStrategy {
    57  	case "sticky":
    58  		consumer.config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategySticky
    59  	case "roundrobin":
    60  		consumer.config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRoundRobin
    61  	case "range":
    62  		consumer.config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
    63  	default:
    64  		err = fmt.Errorf("un define consumer group rebalance strategy")
    65  		return
    66  	}
    67  
    68  	switch consumerOpts.initialOffset {
    69  	case "newest":
    70  		consumer.config.Consumer.Offsets.Initial = sarama.OffsetNewest
    71  	case "oldest":
    72  		consumer.config.Consumer.Offsets.Initial = sarama.OffsetOldest
    73  	default:
    74  		err = fmt.Errorf("un define consumer group rebalance strategy")
    75  		return
    76  	}
    77  
    78  	consumerOpts.AuthOptions.InitSSL(consumer.config)
    79  
    80  	consumerOpts.AuthOptions.InitSASLSCRAM(consumer.config)
    81  
    82  	consumer.client, err = sarama.NewConsumerGroup(consumerOpts.brokerList, consumerOpts.groupId, consumer.config)
    83  	if err != nil {
    84  		err = fmt.Errorf("error creating consumer group client: %v", err)
    85  		return
    86  	}
    87  	log.Info("init consumer group ok!")
    88  
    89  	return
    90  }
    91  
    92  func (consumer *ConsumerGroup) Start() {
    93  	var ctx context.Context
    94  	ctx, consumer.cancel = context.WithCancel(context.Background())
    95  	consumer.startWithContext(ctx)
    96  }
    97  
    98  func (consumer *ConsumerGroup) StartWithTimeOut(timeout time.Duration) {
    99  	var ctx context.Context
   100  	ctx, consumer.cancel = context.WithTimeout(context.Background(), timeout)
   101  	consumer.startWithContext(ctx)
   102  }
   103  
   104  func (consumer *ConsumerGroup) StartWithDeadline(time time.Time) {
   105  	var ctx context.Context
   106  	ctx, consumer.cancel = context.WithDeadline(context.Background(), time)
   107  	consumer.startWithContext(ctx)
   108  }
   109  
   110  func (consumer *ConsumerGroup) startWithContext(ctx context.Context) {
   111  	if consumerGroupNames.Contains(consumer.name) {
   112  		log.Warn("have the same consumer to start name", consumer.name)
   113  		return
   114  	}
   115  	runtimer.GoSafely(nil, false, func() {
   116  		// Track errors
   117  		for err := range consumer.client.Errors() {
   118  			log.Error(err)
   119  		}
   120  	}, nil, nil)
   121  
   122  	runtimer.GoSafely(consumer.wg, false, func() {
   123  		// `Consume` should be called inside an infinite loop, when a
   124  		// server-side rebalance happens, the consumer session will need to be
   125  		// recreated to get the new claims
   126  		for {
   127  			if err := consumer.client.Consume(ctx, consumer.topicList, consumer); err != nil {
   128  				log.Error("Error from consumer: %v", err)
   129  			}
   130  			// check if context was cancelled, signaling that the consumer should stop
   131  			if ctx.Err() != nil {
   132  				log.Info("Sarama consumer stop!... ", time.Now().Format(time.RFC3339))
   133  				return
   134  			}
   135  			consumer.ready = make(chan bool)
   136  		}
   137  	}, nil, nil)
   138  
   139  	<-consumer.ready // Await till the consumer has been set up
   140  	log.Info("Sarama consumer up and running!...")
   141  	consumerGroupNames.Add(consumer.name)
   142  }
   143  
   144  func (consumer *ConsumerGroup) Close() {
   145  	consumer.wg.Wait()
   146  
   147  	if consumer.cancel != nil {
   148  		consumer.cancel()
   149  	}
   150  
   151  	consumerGroupNames.Remove(consumer.name)
   152  	err := consumer.client.Close()
   153  	if err != nil {
   154  		log.Error("consumer.client.Close err", err.Error())
   155  	}
   156  }
   157  
   158  // Setup is run at the beginning of a new session, before ConsumeClaim
   159  func (consumer *ConsumerGroup) Setup(sarama.ConsumerGroupSession) error {
   160  	// Mark the consumer as ready
   161  	close(consumer.ready)
   162  	return nil
   163  }
   164  
   165  // Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
   166  func (consumer *ConsumerGroup) Cleanup(sarama.ConsumerGroupSession) error {
   167  	return nil
   168  }
   169  
   170  // ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().
   171  func (consumer *ConsumerGroup) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
   172  	// NOTE:
   173  	// Do not move the code below to a goroutine.
   174  	// The `ConsumeClaim` itself is called within a goroutine, see:
   175  	// https://github.com/Shopify/sarama/blob/main/consumer_group.go#L27-L29
   176  	for message := range claim.Messages() {
   177  		log.Info(fmt.Sprintf("Message claimed: value = %s, timestamp = %v, topic = %s", string(message.Value), message.Timestamp, message.Topic))
   178  		err := consumer.msg.Consumer(message)
   179  		if err != nil {
   180  			log.Error(fmt.Sprintf("consumer.msg.Consumer error:%s", err.Error()))
   181  			continue
   182  		}
   183  
   184  		//commit msg ack to consumer ok
   185  		session.MarkMessage(message, consumer.msgMeta)
   186  	}
   187  
   188  	return nil
   189  }