github.com/mailgun/holster/v4@v4.20.0/discovery/memberlist.go (about)

     1  package discovery
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"io"
     7  	"net"
     8  	"runtime"
     9  	"sort"
    10  	"strconv"
    11  	"sync"
    12  	"time"
    13  
    14  	ml "github.com/hashicorp/memberlist"
    15  	"github.com/mailgun/holster/v4/clock"
    16  	"github.com/mailgun/holster/v4/errors"
    17  	"github.com/mailgun/holster/v4/retry"
    18  	"github.com/mailgun/holster/v4/setter"
    19  	"github.com/sirupsen/logrus"
    20  )
    21  
    22  type Peer struct {
    23  	// An ID the uniquely identifies this peer
    24  	ID string
    25  	// The metadata associated with this peer
    26  	Metadata []byte
    27  	// Is true if this Peer refers to our instance
    28  	IsSelf bool
    29  }
    30  
    31  type OnUpdateFunc func([]Peer)
    32  
    33  type Members interface {
    34  	// Returns the peers currently registered
    35  	GetPeers(context.Context) ([]Peer, error)
    36  	// Removes our peer from the member list and closes all connections
    37  	Close(context.Context) error
    38  	// TODO: Updates the Peer metadata shared with peers
    39  	// UpdatePeer(context.Context, Peer) error
    40  }
    41  
    42  type MemberList struct {
    43  	log        logrus.FieldLogger
    44  	memberList *ml.Memberlist
    45  	conf       MemberListConfig
    46  	events     *eventDelegate
    47  }
    48  
    49  type MemberListConfig struct {
    50  	// This is the address:port the member list protocol listen for other peers on.
    51  	BindAddress string
    52  	// This is the address:port the member list protocol will advertise to other peers. (Defaults to BindAddress)
    53  	AdvertiseAddress string
    54  	// Metadata about this peer which should be shared with other peers
    55  	Peer Peer
    56  	// A list of peers this member list instance can contact to find other peers.
    57  	KnownPeers []string
    58  	// A callback function which is called when the member list changes.
    59  	OnUpdate OnUpdateFunc
    60  	// If not nil, use this config instead of ml.DefaultLANConfig()
    61  	MemberListConfig *ml.Config
    62  	// An interface through which logging will occur; usually *logrus.Entry
    63  	Logger logrus.FieldLogger
    64  }
    65  
    66  func NewMemberList(ctx context.Context, conf MemberListConfig) (Members, error) {
    67  	setter.SetDefault(&conf.Logger, logrus.WithField("category", "member-list"))
    68  	setter.SetDefault(&conf.AdvertiseAddress, conf.BindAddress)
    69  	if conf.Peer.ID == "" {
    70  		return nil, errors.New("Peer.ID cannot be empty")
    71  	}
    72  	if conf.BindAddress == "" {
    73  		return nil, errors.New("BindAddress cannot be empty")
    74  	}
    75  	conf.Peer.IsSelf = false
    76  
    77  	m := &MemberList{
    78  		log:  conf.Logger,
    79  		conf: conf,
    80  		events: &eventDelegate{
    81  			peers: make(map[string]Peer, 1),
    82  			conf:  conf,
    83  			log:   conf.Logger,
    84  		},
    85  	}
    86  
    87  	// Create the member list config
    88  	config, err := m.newMLConfig(conf)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	// Create a new member list instance
    94  	m.memberList, err = ml.Create(config)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// Attempt to join the member list using a list of known nodes
   100  	err = retry.Until(ctx, retry.Interval(clock.Millisecond*300), func(ctx context.Context, i int) error {
   101  		_, err = m.memberList.Join(m.conf.KnownPeers)
   102  		if err != nil {
   103  			return errors.Wrapf(err, "while joining member list known peers %#v", m.conf.KnownPeers)
   104  		}
   105  		return nil
   106  	})
   107  	return m, errors.Wrap(err, "timed out attempting to join member list")
   108  }
   109  
   110  func (m *MemberList) newMLConfig(conf MemberListConfig) (*ml.Config, error) {
   111  	config := conf.MemberListConfig
   112  	setter.SetDefault(&config, ml.DefaultLANConfig())
   113  	config.Name = conf.Peer.ID
   114  	config.LogOutput = NewLogWriter(conf.Logger)
   115  	config.PushPullInterval = time.Second * 5
   116  
   117  	var err error
   118  	config.BindAddr, config.BindPort, err = splitAddress(conf.BindAddress)
   119  	if err != nil {
   120  		return nil, errors.Wrap(err, "BindAddress=`%s` is invalid;")
   121  	}
   122  
   123  	config.AdvertiseAddr, config.AdvertisePort, err = splitAddress(conf.AdvertiseAddress)
   124  	if err != nil {
   125  		return nil, errors.Wrap(err, "LivelinessAddress=`%s` is invalid;")
   126  	}
   127  
   128  	m.conf.Logger.Debugf("BindAddr: %s Port: %d", config.BindAddr, config.BindPort)
   129  	m.conf.Logger.Debugf("AdvAddr: %s Port: %d", config.AdvertiseAddr, config.AdvertisePort)
   130  	config.Delegate = &delegate{meta: conf.Peer.Metadata}
   131  	config.Events = m.events
   132  	return config, nil
   133  }
   134  
   135  func (m *MemberList) Close(ctx context.Context) error {
   136  	errCh := make(chan error)
   137  	go func() {
   138  		if err := m.memberList.Leave(clock.Second * 30); err != nil {
   139  			errCh <- err
   140  			return
   141  		}
   142  		errCh <- m.memberList.Shutdown()
   143  	}()
   144  
   145  	select {
   146  	case <-ctx.Done():
   147  		return ctx.Err()
   148  	case err := <-errCh:
   149  		return err
   150  	}
   151  }
   152  
   153  func (m *MemberList) GetPeers(_ context.Context) ([]Peer, error) {
   154  	return m.events.GetPeers()
   155  }
   156  
   157  type eventDelegate struct {
   158  	peers map[string]Peer
   159  	log   logrus.FieldLogger
   160  	conf  MemberListConfig
   161  	mutex sync.Mutex
   162  }
   163  
   164  func (e *eventDelegate) NotifyJoin(node *ml.Node) {
   165  	defer e.mutex.Unlock()
   166  	e.mutex.Lock()
   167  	e.peers[node.Name] = Peer{ID: node.Name, Metadata: node.Meta}
   168  	e.callOnUpdate()
   169  }
   170  
   171  func (e *eventDelegate) NotifyLeave(node *ml.Node) {
   172  	defer e.mutex.Unlock()
   173  	e.mutex.Lock()
   174  	delete(e.peers, node.Name)
   175  	e.callOnUpdate()
   176  }
   177  
   178  func (e *eventDelegate) NotifyUpdate(node *ml.Node) {
   179  	defer e.mutex.Unlock()
   180  	e.mutex.Lock()
   181  	e.peers[node.Name] = Peer{ID: node.Name, Metadata: node.Meta}
   182  	e.callOnUpdate()
   183  }
   184  func (e *eventDelegate) GetPeers() ([]Peer, error) {
   185  	defer e.mutex.Unlock()
   186  	e.mutex.Lock()
   187  	return e.getPeers(), nil
   188  }
   189  
   190  func (e *eventDelegate) getPeers() []Peer {
   191  	var peers []Peer
   192  	for _, p := range e.peers {
   193  		if p.ID == e.conf.Peer.ID {
   194  			p.IsSelf = true
   195  		}
   196  		peers = append(peers, p)
   197  	}
   198  	return peers
   199  }
   200  
   201  func (e *eventDelegate) callOnUpdate() {
   202  	if e.conf.OnUpdate == nil {
   203  		return
   204  	}
   205  
   206  	// Sort the results to make it easy to compare peer lists
   207  	peers := e.getPeers()
   208  	sort.Slice(peers, func(i, j int) bool {
   209  		return peers[i].ID < peers[j].ID
   210  	})
   211  
   212  	e.conf.OnUpdate(peers)
   213  }
   214  
   215  type delegate struct {
   216  	meta []byte
   217  }
   218  
   219  func (m *delegate) NodeMeta(int) []byte {
   220  	return m.meta
   221  }
   222  func (m *delegate) NotifyMsg([]byte)                {}
   223  func (m *delegate) GetBroadcasts(int, int) [][]byte { return nil }
   224  func (m *delegate) LocalState(bool) []byte          { return nil }
   225  func (m *delegate) MergeRemoteState([]byte, bool)   {}
   226  
   227  func NewLogWriter(log logrus.FieldLogger) *io.PipeWriter {
   228  	reader, writer := io.Pipe()
   229  
   230  	go func() {
   231  		scanner := bufio.NewScanner(reader)
   232  		for scanner.Scan() {
   233  			log.Info(scanner.Text())
   234  		}
   235  		if err := scanner.Err(); err != nil {
   236  			log.Errorf("Error while reading from Writer: %s", err)
   237  		}
   238  		reader.Close()
   239  	}()
   240  	runtime.SetFinalizer(writer, func(w *io.PipeWriter) {
   241  		w.Close()
   242  	})
   243  
   244  	return writer
   245  }
   246  
   247  func split(addr string) (retHost string, retPort int, reterr error) {
   248  	host, port, err := net.SplitHostPort(addr)
   249  	if err != nil {
   250  		return host, 0, errors.New(" expected format is `address:port`")
   251  	}
   252  
   253  	intPort, err := strconv.Atoi(port)
   254  	if err != nil {
   255  		return host, intPort, errors.Wrap(err, "port must be a number")
   256  	}
   257  	return host, intPort, nil
   258  }
   259  
   260  func splitAddress(addr string) (retHost string, retPort int, reterr error) {
   261  	host, port, err := split(addr)
   262  	if err != nil {
   263  		return "", 0, err
   264  	}
   265  	// Member list requires the address to be an ip address
   266  	if ip := net.ParseIP(host); ip == nil {
   267  		addresses, err := net.LookupHost(host)
   268  		if err != nil {
   269  			return "", 0, errors.Wrapf(err, "while preforming host lookup for '%s'", host)
   270  		}
   271  		if len(addresses) == 0 {
   272  			return "", 0, errors.Wrapf(err, "net.LookupHost() returned no addresses for '%s'", host)
   273  		}
   274  		host = addresses[0]
   275  	}
   276  	return host, port, nil
   277  }