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 }