github.com/decred/dcrlnd@v0.7.6/kvdb/etcd/embed.go (about)

     1  //go:build kvdb_etcd
     2  // +build kvdb_etcd
     3  
     4  package etcd
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"net/url"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"go.etcd.io/etcd/server/v3/embed"
    14  )
    15  
    16  const (
    17  	// readyTimeout is the time until the embedded etcd instance should start.
    18  	readyTimeout = 10 * time.Second
    19  
    20  	// defaultEtcdPort is the start of the range for listening ports of
    21  	// embedded etcd servers. Ports are monotonically increasing starting
    22  	// from this number and are determined by the results of getFreePort().
    23  	defaultEtcdPort = 2379
    24  
    25  	// defaultNamespace is the namespace we'll use in our embedded etcd
    26  	// instance. Since it is only used for testing, we'll use the namespace
    27  	// name "test/" for this. Note that the namespace can be any string,
    28  	// the trailing / is not required.
    29  	defaultNamespace = "test/"
    30  )
    31  
    32  var (
    33  	// lastPort is the last port determined to be free for use by a new
    34  	// embedded etcd server. It should be used atomically.
    35  	lastPort uint32 = defaultEtcdPort
    36  )
    37  
    38  // getFreePort returns the first port that is available for listening by a new
    39  // embedded etcd server. It panics if no port is found and the maximum available
    40  // TCP port is reached.
    41  func getFreePort() int {
    42  	port := atomic.AddUint32(&lastPort, 1)
    43  	for port < 65535 {
    44  		// If there are no errors while attempting to listen on this
    45  		// port, close the socket and return it as available.
    46  		addr := fmt.Sprintf("127.0.0.1:%d", port)
    47  		l, err := net.Listen("tcp4", addr)
    48  		if err == nil {
    49  			err := l.Close()
    50  			if err == nil {
    51  				return int(port)
    52  			}
    53  		}
    54  		port = atomic.AddUint32(&lastPort, 1)
    55  	}
    56  
    57  	// No ports available? Must be a mistake.
    58  	panic("no ports available for listening")
    59  }
    60  
    61  // NewEmbeddedEtcdInstance creates an embedded etcd instance for testing,
    62  // listening on random open ports. Returns the backend config and a cleanup
    63  // func that will stop the etcd instance.
    64  func NewEmbeddedEtcdInstance(path string, clientPort, peerPort uint16,
    65  	logFile string) (*Config, func(), error) {
    66  
    67  	cfg := embed.NewConfig()
    68  	cfg.Dir = path
    69  
    70  	// To ensure that we can submit large transactions.
    71  	cfg.MaxTxnOps = 8192
    72  	cfg.MaxRequestBytes = 16384 * 1024
    73  	cfg.Logger = "zap"
    74  	if logFile != "" {
    75  		cfg.LogLevel = "info"
    76  		cfg.LogOutputs = []string{logFile}
    77  	} else {
    78  		cfg.LogLevel = "error"
    79  	}
    80  
    81  	// Listen on random free ports if no ports were specified.
    82  	if clientPort == 0 {
    83  		clientPort = uint16(getFreePort())
    84  	}
    85  
    86  	if peerPort == 0 {
    87  		peerPort = uint16(getFreePort())
    88  	}
    89  
    90  	clientURL := fmt.Sprintf("127.0.0.1:%d", clientPort)
    91  	peerURL := fmt.Sprintf("127.0.0.1:%d", peerPort)
    92  	cfg.LCUrls = []url.URL{{Host: clientURL}}
    93  	cfg.LPUrls = []url.URL{{Host: peerURL}}
    94  
    95  	etcd, err := embed.StartEtcd(cfg)
    96  	if err != nil {
    97  		return nil, nil, err
    98  	}
    99  
   100  	select {
   101  	case <-etcd.Server.ReadyNotify():
   102  	case <-time.After(readyTimeout):
   103  		etcd.Close()
   104  		return nil, nil,
   105  			fmt.Errorf("etcd failed to start after: %v", readyTimeout)
   106  	}
   107  
   108  	connConfig := &Config{
   109  		Host:               "http://" + clientURL,
   110  		InsecureSkipVerify: true,
   111  		Namespace:          defaultNamespace,
   112  	}
   113  
   114  	return connConfig, func() {
   115  		etcd.Close()
   116  	}, nil
   117  }