github.com/rudderlabs/rudder-go-kit@v0.30.0/kafkaclient/producer.go (about) 1 package client 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "syscall" 10 "time" 11 12 "github.com/segmentio/kafka-go" 13 ) 14 15 type Compression = kafka.Compression 16 17 const ( 18 CompressionNone Compression = 0 19 CompressionGzip Compression = kafka.Gzip 20 CompressionSnappy Compression = kafka.Snappy 21 CompressionLz4 Compression = kafka.Lz4 22 CompressionZstd Compression = kafka.Zstd 23 ) 24 25 type ProducerConfig struct { 26 ClientID string 27 WriteTimeout, 28 ReadTimeout, 29 BatchTimeout time.Duration 30 BatchSize int 31 Compression Compression 32 Logger Logger 33 ErrorLogger Logger 34 } 35 36 func (c *ProducerConfig) defaults() { 37 if c.WriteTimeout < 1 { 38 c.WriteTimeout = 10 * time.Second 39 } 40 if c.ReadTimeout < 1 { 41 c.ReadTimeout = 10 * time.Second 42 } 43 if c.BatchTimeout < 1 { 44 c.BatchTimeout = time.Nanosecond 45 } 46 if c.BatchSize < 1 { 47 // this is like disabling batching. 48 // these settings are overridden only if "Router.KAFKA.enableBatching" is true and in that case 49 // other default values are used (see kafkamanager.go). 50 c.BatchSize = 1 51 } 52 } 53 54 // Producer provides a high-level API for producing messages to Kafka 55 type Producer struct { 56 writer *kafka.Writer 57 config ProducerConfig 58 } 59 60 // NewProducer instantiates a new producer. To use it asynchronously just do "go p.Publish(ctx, msgs)". 61 func (c *Client) NewProducer(producerConf ProducerConfig) (p *Producer, err error) { // skipcq: CRT-P0003 62 producerConf.defaults() 63 64 transport := &kafka.Transport{ 65 DialTimeout: c.config.DialTimeout, 66 Dial: c.dialer.DialFunc, 67 } 68 if producerConf.ClientID != "" { 69 transport.ClientID = producerConf.ClientID 70 } else if c.config.ClientID != "" { 71 transport.ClientID = c.config.ClientID 72 } 73 if c.config.TLS != nil { 74 transport.TLS, err = c.config.TLS.build() 75 if err != nil { 76 return nil, fmt.Errorf("could not build TLS configuration: %w", err) 77 } 78 } 79 if c.config.SASL != nil { 80 transport.SASL, err = c.config.SASL.build() 81 if err != nil { 82 return nil, fmt.Errorf("could not build SASL configuration: %w", err) 83 } 84 } 85 86 p = &Producer{ 87 config: producerConf, 88 writer: &kafka.Writer{ 89 Addr: kafka.TCP(c.addresses...), 90 Balancer: &kafka.ReferenceHash{}, 91 WriteTimeout: producerConf.WriteTimeout, 92 ReadTimeout: producerConf.ReadTimeout, 93 BatchTimeout: producerConf.BatchTimeout, 94 BatchSize: producerConf.BatchSize, 95 MaxAttempts: 3, 96 RequiredAcks: kafka.RequireAll, 97 AllowAutoTopicCreation: true, 98 Async: false, 99 Compression: producerConf.Compression, 100 Transport: transport, 101 }, 102 } 103 return 104 } 105 106 // Close tries to close the producer, but it will return sooner if the context is canceled. 107 // A routine in background will still try to close the producer since the underlying library does not support 108 // contexts on Close(). 109 func (p *Producer) Close(ctx context.Context) error { 110 done := make(chan error, 1) 111 go func() { 112 if p.writer != nil { 113 done <- p.writer.Close() 114 } 115 close(done) 116 }() 117 118 select { 119 case <-ctx.Done(): 120 return ctx.Err() 121 case err := <-done: 122 return err 123 } 124 } 125 126 // Publish allows the production of one or more message to Kafka. 127 // To use it asynchronously just do "go p.Publish(ctx, msgs)". 128 func (p *Producer) Publish(ctx context.Context, msgs ...Message) error { 129 messages := make([]kafka.Message, len(msgs)) 130 for i := range msgs { 131 if msgs[i].Topic == "" { 132 return fmt.Errorf("no topic provided for message %d", i) 133 } 134 headers := headers(msgs[i]) 135 messages[i] = kafka.Message{ 136 Topic: msgs[i].Topic, 137 Key: msgs[i].Key, 138 Value: msgs[i].Value, 139 Time: msgs[i].Timestamp, 140 Headers: headers, 141 } 142 } 143 144 return p.writer.WriteMessages(ctx, messages...) 145 } 146 147 func headers(msg Message) (headers []kafka.Header) { 148 if l := len(msg.Headers); l > 0 { 149 headers = make([]kafka.Header, l) 150 for k := range msg.Headers { 151 headers[k] = kafka.Header{ 152 Key: msg.Headers[k].Key, 153 Value: msg.Headers[k].Value, 154 } 155 } 156 } 157 return headers 158 } 159 160 func isErrTemporary(err error) bool { 161 isTransientNetworkError := errors.Is(err, io.ErrUnexpectedEOF) || 162 errors.Is(err, syscall.ECONNREFUSED) || 163 errors.Is(err, syscall.ECONNRESET) || 164 errors.Is(err, syscall.EPIPE) 165 if isTransientNetworkError { 166 return true 167 } 168 var tempError interface{ Temporary() bool } 169 if errors.As(err, &tempError) { 170 return tempError.Temporary() 171 } 172 if os.IsTimeout(err) { 173 return true 174 } 175 return false 176 } 177 178 func IsProducerErrTemporary(err error) bool { 179 var we kafka.WriteErrors 180 if errors.As(err, &we) { 181 for _, err := range we { 182 // if at least one was temporary then we treat the whole batch as such 183 if isErrTemporary(err) { 184 return true 185 } 186 } 187 return false 188 } 189 return isErrTemporary(err) 190 }