github.com/rudderlabs/rudder-go-kit@v0.30.0/kafkaclient/testutil/util.go (about)

     1  package testutil
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/segmentio/kafka-go"
    11  )
    12  
    13  // NewWithDialer returns a client with a custom dialer
    14  func NewWithDialer(dialer *kafka.Dialer, network string, address ...string) *Client {
    15  	return &Client{
    16  		dialer:    dialer,
    17  		network:   network,
    18  		addresses: address,
    19  	}
    20  }
    21  
    22  // New returns a client with a default dialer
    23  func New(network, address string) *Client {
    24  	return NewWithDialer(&kafka.Dialer{
    25  		DualStack: true,
    26  		Timeout:   10 * time.Second,
    27  	}, network, address)
    28  }
    29  
    30  type Client struct {
    31  	dialer    *kafka.Dialer
    32  	network   string
    33  	addresses []string
    34  }
    35  
    36  func (c *Client) ping(ctx context.Context) (*kafka.Conn, error) {
    37  	var (
    38  		err  error
    39  		conn *kafka.Conn
    40  	)
    41  	for _, addr := range c.addresses {
    42  		conn, err = c.dialer.DialContext(ctx, c.network, kafka.TCP(addr).String())
    43  		if err == nil { // we can connect to at least one address, no need to check all of them
    44  			break
    45  		}
    46  	}
    47  	if err != nil {
    48  		return nil, fmt.Errorf(
    49  			"could not dial any of the addresses %s/%s: %w", c.network, kafka.TCP(c.addresses...).String(), err,
    50  		)
    51  	}
    52  	return conn, nil
    53  }
    54  
    55  func (c *Client) CreateTopic(ctx context.Context, topic string, numPartitions, replicationFactor int) error {
    56  	conn, err := c.ping(ctx)
    57  	if err != nil {
    58  		return fmt.Errorf("create topic: cannot ping: %w", err)
    59  	}
    60  
    61  	var (
    62  		controllerHost string
    63  		errors         = make(chan error, 1)
    64  	)
    65  	defer close(errors)
    66  	go func() { // doing it asynchronously because conn.Controller() does not honour the context
    67  		var b kafka.Broker
    68  		b, err = conn.Controller()
    69  		if err != nil {
    70  			errors <- fmt.Errorf("create topic: could not get controller: %w", err)
    71  			return
    72  		}
    73  		if b.Host == "" {
    74  			errors <- fmt.Errorf("create topic: controller connection has empty broker host")
    75  			return
    76  		}
    77  		controllerHost = net.JoinHostPort(b.Host, strconv.Itoa(b.Port))
    78  		errors <- nil
    79  	}()
    80  
    81  	select {
    82  	case <-ctx.Done():
    83  		return fmt.Errorf("create topic: %w", ctx.Err())
    84  	case err := <-errors:
    85  		if err != nil {
    86  			return err
    87  		}
    88  	}
    89  
    90  	controllerConn, err := c.dialer.DialContext(ctx, c.network, controllerHost)
    91  	if err != nil {
    92  		return fmt.Errorf("create topic: could not dial controller: %w", err)
    93  	}
    94  	defer func() {
    95  		// close asynchronously, if we block we might not respect the context
    96  		go func() { _ = controllerConn.Close() }()
    97  	}()
    98  
    99  	go func() { // doing it asynchronously because controllerConn.CreateTopics() does not honour the context
   100  		errors <- controllerConn.CreateTopics(kafka.TopicConfig{
   101  			Topic:             topic,
   102  			NumPartitions:     numPartitions,
   103  			ReplicationFactor: replicationFactor,
   104  		})
   105  	}()
   106  
   107  	select {
   108  	case <-ctx.Done():
   109  		return ctx.Err()
   110  	case err = <-errors:
   111  		if err != nil {
   112  			return fmt.Errorf("create topic: cannot create topic with controller %q and addresses %+v: %w",
   113  				controllerHost, c.addresses, err,
   114  			)
   115  		}
   116  		return nil
   117  	}
   118  }
   119  
   120  type TopicPartition struct {
   121  	Topic     string
   122  	Partition int
   123  }
   124  
   125  func (c *Client) ListTopics(ctx context.Context) ([]TopicPartition, error) {
   126  	conn, err := c.ping(ctx)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	var (
   132  		done   = make(chan []kafka.Partition, 1)
   133  		errors = make(chan error, 1)
   134  	)
   135  	go func() { // doing it asynchronously because conn.ReadPartitions() does not honour the context
   136  		partitions, err := conn.ReadPartitions()
   137  		if err != nil {
   138  			errors <- err
   139  		} else {
   140  			done <- partitions
   141  		}
   142  	}()
   143  
   144  	select {
   145  	case <-ctx.Done():
   146  		return nil, ctx.Err()
   147  	case err = <-errors:
   148  		return nil, fmt.Errorf("could not read partitions: %w", err)
   149  	case partitions := <-done:
   150  		var topics []TopicPartition
   151  		for i := range partitions {
   152  			topics = append(topics, TopicPartition{
   153  				Topic:     partitions[i].Topic,
   154  				Partition: partitions[i].ID,
   155  			})
   156  		}
   157  		return topics, nil
   158  	}
   159  }