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 }