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  }