github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/kafka/kafka.go (about)

     1  package kafka
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  
     9  	"github.com/IBM/sarama"
    10  	"github.com/bingoohuang/gg/pkg/ss"
    11  )
    12  
    13  type ProducerConfig struct {
    14  	Topic           string
    15  	Version         string
    16  	Brokers         []string
    17  	Codec           string
    18  	MaxMessageBytes int
    19  	Sync            bool
    20  	RequiredAcks    sarama.RequiredAcks
    21  
    22  	TlsConfig TlsConfig
    23  	Context   context.Context
    24  
    25  	SASLUser     string
    26  	SASLPassword string
    27  	SASLVersion  *int
    28  }
    29  
    30  type PublishResponse struct {
    31  	Result interface{}
    32  	Topic  string
    33  }
    34  
    35  type Options struct {
    36  	MessageKey string
    37  	Headers    map[string]string
    38  }
    39  
    40  func (o *Options) Fulfil(msg *sarama.ProducerMessage) {
    41  	if len(o.Headers) > 0 {
    42  		msg.Headers = make([]sarama.RecordHeader, 0, len(o.Headers))
    43  		for k, v := range o.Headers {
    44  			msg.Headers = append(msg.Headers, sarama.RecordHeader{Key: []byte(k), Value: []byte(v)})
    45  		}
    46  	}
    47  
    48  	if len(o.MessageKey) > 0 {
    49  		msg.Key = sarama.StringEncoder(o.MessageKey)
    50  	}
    51  }
    52  
    53  type OptionFn func(*Options)
    54  
    55  func WithKey(key string) OptionFn     { return func(options *Options) { options.MessageKey = key } }
    56  func WithHeader(k, v string) OptionFn { return func(options *Options) { options.Headers[k] = v } }
    57  
    58  type asyncProducer struct {
    59  	sarama.AsyncProducer
    60  }
    61  
    62  func (p asyncProducer) SendMessage(msg *sarama.ProducerMessage) (interface{}, error) {
    63  	p.Input() <- msg
    64  	return AsyncProducerResult{Enqueued: true}, nil
    65  }
    66  
    67  type AsyncProducerResult struct {
    68  	Enqueued    bool
    69  	ContextDone bool
    70  }
    71  
    72  type SyncProducerResult struct {
    73  	Partition int32
    74  	Offset    int64
    75  }
    76  
    77  type syncProducer struct {
    78  	sarama.SyncProducer
    79  }
    80  
    81  func (p syncProducer) SendMessage(msg *sarama.ProducerMessage) (interface{}, error) {
    82  	partition, offset, err := p.SyncProducer.SendMessage(msg)
    83  	return SyncProducerResult{Partition: partition, Offset: offset}, err
    84  }
    85  
    86  type producer interface {
    87  	SendMessage(*sarama.ProducerMessage) (interface{}, error)
    88  }
    89  
    90  type Producer struct {
    91  	producer
    92  	io.Closer
    93  	Config *ProducerConfig
    94  }
    95  
    96  func (p Producer) Publish(topic string, vv []byte, optionsFns ...OptionFn) (*PublishResponse, error) {
    97  	options := &Options{Headers: make(map[string]string)}
    98  	for _, f := range optionsFns {
    99  		f(options)
   100  	}
   101  
   102  	// We are not setting a message key, which means that all messages will
   103  	// be distributed randomly over the different partitions.
   104  	msg := &sarama.ProducerMessage{Topic: ss.Or(topic, p.Config.Topic), Value: sarama.ByteEncoder(vv)}
   105  	options.Fulfil(msg)
   106  
   107  	result, err := p.producer.SendMessage(msg)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	return &PublishResponse{Result: result, Topic: msg.Topic}, nil
   113  }
   114  
   115  func (c *ProducerConfig) NewProducer() (*Producer, error) {
   116  	// For the data collector, we are looking for strong consistency semantics.
   117  	// Because we don't change the flush settings, sarama will try to produce messages
   118  	// as fast as possible to keep latency low.
   119  	sc := sarama.NewConfig()
   120  	if err := ParseVersion(sc, c.Version); err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	sc.Producer.Compression = ParseCodec(c.Codec)
   125  	sc.Producer.RequiredAcks = c.RequiredAcks
   126  	sc.Producer.Retry.Max = 3 // Retry up to x times to produce the message
   127  	if c.MaxMessageBytes > 0 {
   128  		sc.Producer.MaxMessageBytes = c.MaxMessageBytes
   129  	} else {
   130  		sc.Producer.MaxMessageBytes = int(sarama.MaxRequestSize)
   131  	}
   132  	sc.Producer.Return.Successes = true
   133  	if tc := c.TlsConfig.Create(); tc != nil {
   134  		sc.Net.TLS.Config = tc
   135  		sc.Net.TLS.Enable = true
   136  	}
   137  
   138  	if c.SASLUser != "" {
   139  		sc.Net.SASL.Enable = true
   140  		sc.Net.SASL.User = c.SASLUser
   141  		sc.Net.SASL.Password = c.SASLPassword
   142  		sc.Net.SASL.Handshake = true
   143  		sc.Net.SASL.Mechanism = sarama.SASLTypePlaintext
   144  		version, err := SASLVersion(sc.Version, c.SASLVersion)
   145  		if err != nil {
   146  			return nil, err
   147  		}
   148  		sc.Net.SASL.Version = version
   149  	}
   150  
   151  	// On the broker side, you may want to change the following settings to get
   152  	// stronger consistency guarantees:
   153  	// - For your broker, set `unclean.leader.election.enable` to false
   154  	// - For the topic, you could increase `min.insync.replicas`.
   155  	if c.Sync {
   156  		p, err := sarama.NewSyncProducer(c.Brokers, sc)
   157  		if err != nil {
   158  			return nil, fmt.Errorf("failed to start Sarama SyncProducer, %w", err)
   159  		}
   160  		return &Producer{producer: &syncProducer{SyncProducer: p}, Config: c}, nil
   161  	}
   162  
   163  	sc.Producer.Return.Errors = true
   164  	p, err := sarama.NewAsyncProducer(c.Brokers, sc)
   165  	if err != nil {
   166  		return nil, fmt.Errorf("failed to start Sarama NewAsyncProducer, %w", err)
   167  	}
   168  	// We will just log to STDOUT if we're not able to produce messages.
   169  	// Note: messages will only be returned here after all retry attempts are exhausted.
   170  	go func() {
   171  		for {
   172  			select {
   173  			case <-c.Context.Done():
   174  				return
   175  			case <-p.Successes():
   176  			case err := <-p.Errors():
   177  				log.Println("Failed to write access log entry:", err)
   178  			}
   179  		}
   180  	}()
   181  
   182  	return &Producer{producer: &asyncProducer{AsyncProducer: p}, Config: c}, nil
   183  }