gossip registry

This commit is contained in:
Asim Aslam 2018-12-04 16:41:40 +00:00
parent d0d8db7c45
commit 57dcba666e
9 changed files with 775 additions and 0 deletions

375
registry/gossip/gossip.go Normal file
View File

@ -0,0 +1,375 @@
// Package gossip provides a zero dependency registry using the gossip protocol SWIM
package gossip
import (
"encoding/json"
"os"
"strings"
"sync"
"time"
"github.com/golang/protobuf/proto"
"github.com/google/uuid"
"github.com/hashicorp/memberlist"
"github.com/micro/go-log"
"github.com/micro/go-micro/registry"
pb "github.com/micro/go-micro/registry/gossip/proto"
)
type gossipRegistry struct {
opts registry.Options
queue *memberlist.TransmitLimitedQueue
memberlist *memberlist.Memberlist
delegate *delegate
sync.RWMutex
services map[string][]*registry.Service
watchers map[string]*watcher
}
var (
defaultPort = 8118
)
type broadcast struct {
update *pb.Update
notify chan<- struct{}
}
type delegate struct {
queue *memberlist.TransmitLimitedQueue
registry *gossipRegistry
}
func (b *broadcast) Invalidates(other memberlist.Broadcast) bool {
up := new(pb.Update)
if err := proto.Unmarshal(other.Message(), up); err != nil {
return false
}
// ids do not match
if b.update.Id == up.Id {
return false
}
// timestamps do not match
if b.update.Timestamp != up.Timestamp {
return false
}
// type does not match
if b.update.Type != up.Type {
return false
}
// invalidates
return true
}
func (b *broadcast) Message() []byte {
up, err := proto.Marshal(b.update)
if err != nil {
return nil
}
return up
}
func (b *broadcast) Finished() {
if b.notify != nil {
close(b.notify)
}
}
func (d *delegate) NodeMeta(limit int) []byte {
return []byte{}
}
func (d *delegate) NotifyMsg(b []byte) {
if len(b) == 0 {
return
}
up := new(pb.Update)
if err := proto.Unmarshal(b, up); err != nil {
return
}
// only process service action
if up.Type != "service" {
return
}
var service *registry.Service
switch up.Metadata["Content-Type"] {
case "application/json":
if err := json.Unmarshal(up.Data, &service); err != nil {
return
}
// no other content type
default:
return
}
d.registry.Lock()
defer d.registry.Unlock()
// get existing service
s := d.registry.services[service.Name]
// save update
switch up.Action {
case "update":
d.registry.services[service.Name] = addServices(s, []*registry.Service{service})
case "delete":
services := delServices(s, []*registry.Service{service})
if len(services) == 0 {
delete(d.registry.services, service.Name)
return
}
d.registry.services[service.Name] = services
default:
return
}
// notify watchers
for _, w := range d.registry.watchers {
select {
case w.ch <- &registry.Result{Action: up.Action, Service: service}:
default:
}
}
}
func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte {
return d.queue.GetBroadcasts(overhead, limit)
}
func (d *delegate) LocalState(join bool) []byte {
d.registry.RLock()
b, _ := json.Marshal(d.registry.services)
d.registry.RUnlock()
return b
}
func (d *delegate) MergeRemoteState(buf []byte, join bool) {
if len(buf) == 0 {
return
}
if !join {
return
}
var services map[string][]*registry.Service
if err := json.Unmarshal(buf, &services); err != nil {
return
}
d.registry.Lock()
for k, v := range services {
d.registry.services[k] = addServices(d.registry.services[k], v)
}
d.registry.Unlock()
}
func (g *gossipRegistry) Init(opts ...registry.Option) error {
addrs := g.opts.Addrs
for _, o := range opts {
o(&g.opts)
}
// if we have memberlist join it
if len(addrs) != len(g.opts.Addrs) {
_, err := g.memberlist.Join(g.opts.Addrs)
if err != nil {
return err
}
}
return nil
}
func (g *gossipRegistry) Options() registry.Options {
return g.opts
}
func (g *gossipRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
b, err := json.Marshal(s)
if err != nil {
return err
}
g.Lock()
g.services[s.Name] = addServices(g.services[s.Name], []*registry.Service{s})
g.Unlock()
up := &pb.Update{
Id: uuid.New().String(),
Timestamp: uint64(time.Now().UnixNano()),
Action: "update",
Type: "service",
Metadata: map[string]string{
"Content-Type": "application/json",
},
Data: b,
}
g.queue.QueueBroadcast(&broadcast{
update: up,
notify: nil,
})
return nil
}
func (g *gossipRegistry) Deregister(s *registry.Service) error {
b, err := json.Marshal(s)
if err != nil {
return err
}
g.Lock()
g.services[s.Name] = delServices(g.services[s.Name], []*registry.Service{s})
g.Unlock()
up := &pb.Update{
Id: uuid.New().String(),
Timestamp: uint64(time.Now().UnixNano()),
Action: "delete",
Type: "service",
Metadata: map[string]string{
"Content-Type": "application/json",
},
Data: b,
}
g.queue.QueueBroadcast(&broadcast{
update: up,
notify: nil,
})
return nil
}
func (g *gossipRegistry) GetService(name string) ([]*registry.Service, error) {
g.RLock()
if s, ok := g.services[name]; ok {
service := cp(s)
g.RUnlock()
return service, nil
}
g.RUnlock()
return nil, registry.ErrNotFound
}
func (g *gossipRegistry) ListServices() ([]*registry.Service, error) {
var services []*registry.Service
g.RLock()
for name, _ := range g.services {
services = append(services, &registry.Service{Name: name})
}
g.RUnlock()
return services, nil
}
func (g *gossipRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
var options registry.WatchOptions
for _, o := range opts {
o(&options)
}
// watcher id
id := uuid.New().String()
// create watcher
w := &watcher{
ch: make(chan *registry.Result, 1),
exit: make(chan bool),
id: id,
// filter service
srv: options.Service,
// delete self
fn: func() {
g.Lock()
delete(g.watchers, id)
g.Unlock()
},
}
// save watcher
g.Lock()
g.watchers[w.id] = w
g.Unlock()
return w, nil
}
func (g *gossipRegistry) String() string {
return "gossip"
}
func (g *gossipRegistry) run() error {
hostname, _ := os.Hostname()
// delegates
d := new(delegate)
// create a new default config
c := memberlist.DefaultLocalConfig()
// assign the delegate
c.Delegate = d
// Set the bind port
c.BindPort = defaultPort
// set the name
c.Name = strings.Join([]string{"micro", hostname, uuid.New().String()}, "-")
// TODO: set advertise addr to advertise behind nat
// create the memberlist
m, err := memberlist.Create(c)
if err != nil {
return err
}
// if we have memberlist join it
if len(g.opts.Addrs) > 0 {
_, err := m.Join(g.opts.Addrs)
if err != nil {
return err
}
}
// Set the broadcast limit and number of nodes
d.queue = &memberlist.TransmitLimitedQueue{
NumNodes: func() int {
return m.NumMembers()
},
RetransmitMult: 3,
}
g.memberlist = m
g.delegate = d
d.registry = g
return nil
}
// NewRegistry returns a new gossip registry
func NewRegistry(opts ...registry.Option) registry.Registry {
var options registry.Options
for _, o := range opts {
o(&options)
}
g := &gossipRegistry{
opts: options,
}
if err := g.run(); err != nil {
log.Fatal(err)
}
// return gossip registry
return g
}

View File

@ -0,0 +1,28 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: github.com/micro/go-micro/registry/gossip/proto/gossip.proto
/*
Package gossip is a generated protocol buffer package.
It is generated from these files:
github.com/micro/go-micro/registry/gossip/proto/gossip.proto
It has these top-level messages:
Update
*/
package gossip
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package

View File

@ -0,0 +1,118 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/micro/go-micro/registry/gossip/proto/gossip.proto
/*
Package gossip is a generated protocol buffer package.
It is generated from these files:
github.com/micro/go-micro/registry/gossip/proto/gossip.proto
It has these top-level messages:
Update
*/
package gossip
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// Update is the message broadcast
type Update struct {
// unique id of update
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
// unix nano timestamp of update
Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp" json:"timestamp,omitempty"`
// type of update; service
Type string `protobuf:"bytes,3,opt,name=type" json:"type,omitempty"`
// what action is taken; add, del, put
Action string `protobuf:"bytes,4,opt,name=action" json:"action,omitempty"`
// any other associated metadata about the data
Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// the payload data;
Data []byte `protobuf:"bytes,6,opt,name=data,proto3" json:"data,omitempty"`
}
func (m *Update) Reset() { *m = Update{} }
func (m *Update) String() string { return proto.CompactTextString(m) }
func (*Update) ProtoMessage() {}
func (*Update) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *Update) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Update) GetTimestamp() uint64 {
if m != nil {
return m.Timestamp
}
return 0
}
func (m *Update) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *Update) GetAction() string {
if m != nil {
return m.Action
}
return ""
}
func (m *Update) GetMetadata() map[string]string {
if m != nil {
return m.Metadata
}
return nil
}
func (m *Update) GetData() []byte {
if m != nil {
return m.Data
}
return nil
}
func init() {
proto.RegisterType((*Update)(nil), "gossip.Update")
}
func init() {
proto.RegisterFile("github.com/micro/go-micro/registry/gossip/proto/gossip.proto", fileDescriptor0)
}
var fileDescriptor0 = []byte{
// 237 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0xcf, 0x4a, 0xc4, 0x30,
0x10, 0xc6, 0x49, 0xdb, 0x0d, 0x76, 0xfc, 0x83, 0x0c, 0x22, 0x41, 0xf6, 0x50, 0x3c, 0xf5, 0x62,
0x0b, 0x7a, 0x59, 0xd4, 0xab, 0x47, 0x2f, 0x01, 0x1f, 0x20, 0xdb, 0x86, 0x1a, 0x34, 0x9b, 0x90,
0xce, 0x0a, 0x7d, 0x68, 0xdf, 0x41, 0x9a, 0x04, 0xc5, 0xdb, 0xef, 0x37, 0xf9, 0xc2, 0x7c, 0x03,
0xcf, 0x93, 0xa1, 0xf7, 0xe3, 0xbe, 0x1b, 0x9c, 0xed, 0xad, 0x19, 0x82, 0xeb, 0x27, 0x77, 0x97,
0x20, 0xe8, 0xc9, 0xcc, 0x14, 0x96, 0x7e, 0x72, 0xf3, 0x6c, 0x7c, 0xef, 0x83, 0x23, 0x97, 0xa5,
0x8b, 0x82, 0x3c, 0xd9, 0xed, 0x37, 0x03, 0xfe, 0xe6, 0x47, 0x45, 0x1a, 0x2f, 0xa0, 0x30, 0xa3,
0x60, 0x0d, 0x6b, 0x6b, 0x59, 0x98, 0x11, 0xb7, 0x50, 0x93, 0xb1, 0x7a, 0x26, 0x65, 0xbd, 0x28,
0x1a, 0xd6, 0x56, 0xf2, 0x6f, 0x80, 0x08, 0x15, 0x2d, 0x5e, 0x8b, 0x32, 0xe6, 0x23, 0xe3, 0x35,
0x70, 0x35, 0x90, 0x71, 0x07, 0x51, 0xc5, 0x69, 0x36, 0xdc, 0xc1, 0x89, 0xd5, 0xa4, 0x46, 0x45,
0x4a, 0x6c, 0x9a, 0xb2, 0x3d, 0xbd, 0xdf, 0x76, 0xb9, 0x4d, 0xda, 0xdd, 0xbd, 0xe6, 0xe7, 0x97,
0x03, 0x85, 0x45, 0xfe, 0xa6, 0xd7, 0x2d, 0xf1, 0x17, 0x6f, 0x58, 0x7b, 0x26, 0x23, 0xdf, 0x3c,
0xc1, 0xf9, 0xbf, 0x38, 0x5e, 0x42, 0xf9, 0xa1, 0x97, 0xdc, 0x7c, 0x45, 0xbc, 0x82, 0xcd, 0x97,
0xfa, 0x3c, 0xea, 0x58, 0xbb, 0x96, 0x49, 0x1e, 0x8b, 0x1d, 0xdb, 0xf3, 0x78, 0xfe, 0xc3, 0x4f,
0x00, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x49, 0xa9, 0xd7, 0x3e, 0x01, 0x00, 0x00,
}

View File

@ -0,0 +1,19 @@
syntax = "proto3";
package gossip;
// Update is the message broadcast
message Update {
// unique id of update
string id = 1;
// unix nano timestamp of update
uint64 timestamp = 2;
// type of update; service
string type = 3;
// what action is taken; add, del, put
string action = 4;
// any other associated metadata about the data
map<string, string> metadata = 5;
// the payload data;
bytes data = 6;
}

109
registry/gossip/util.go Normal file
View File

@ -0,0 +1,109 @@
package gossip
import (
"github.com/micro/go-micro/registry"
)
func cp(current []*registry.Service) []*registry.Service {
var services []*registry.Service
for _, service := range current {
// copy service
s := new(registry.Service)
*s = *service
// copy nodes
var nodes []*registry.Node
for _, node := range service.Nodes {
n := new(registry.Node)
*n = *node
nodes = append(nodes, n)
}
s.Nodes = nodes
// copy endpoints
var eps []*registry.Endpoint
for _, ep := range service.Endpoints {
e := new(registry.Endpoint)
*e = *ep
eps = append(eps, e)
}
s.Endpoints = eps
// append service
services = append(services, s)
}
return services
}
func addNodes(old, neu []*registry.Node) []*registry.Node {
for _, n := range neu {
var seen bool
for i, o := range old {
if o.Id == n.Id {
seen = true
old[i] = n
break
}
}
if !seen {
old = append(old, n)
}
}
return old
}
func addServices(old, neu []*registry.Service) []*registry.Service {
for _, s := range neu {
var seen bool
for i, o := range old {
if o.Version == s.Version {
s.Nodes = addNodes(o.Nodes, s.Nodes)
seen = true
old[i] = s
break
}
}
if !seen {
old = append(old, s)
}
}
return old
}
func delNodes(old, del []*registry.Node) []*registry.Node {
var nodes []*registry.Node
for _, o := range old {
var rem bool
for _, n := range del {
if o.Id == n.Id {
rem = true
break
}
}
if !rem {
nodes = append(nodes, o)
}
}
return nodes
}
func delServices(old, del []*registry.Service) []*registry.Service {
var services []*registry.Service
for i, o := range old {
var rem bool
for _, s := range del {
if o.Version == s.Version {
old[i].Nodes = delNodes(o.Nodes, s.Nodes)
if len(old[i].Nodes) == 0 {
rem = true
}
}
}
if !rem {
services = append(services, o)
}
}
return services
}

View File

@ -0,0 +1,78 @@
package gossip
import (
"testing"
"github.com/micro/go-micro/registry"
)
func TestDelServices(t *testing.T) {
services := []*registry.Service{
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-123",
Address: "localhost",
Port: 9999,
},
},
},
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-123",
Address: "localhost",
Port: 6666,
},
},
},
}
servs := delServices([]*registry.Service{services[0]}, []*registry.Service{services[1]})
if i := len(servs); i > 0 {
t.Errorf("Expected 0 nodes, got %d: %+v", i, servs)
}
t.Logf("Services %+v", servs)
}
func TestDelNodes(t *testing.T) {
services := []*registry.Service{
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-123",
Address: "localhost",
Port: 9999,
},
{
Id: "foo-321",
Address: "localhost",
Port: 6666,
},
},
},
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-123",
Address: "localhost",
Port: 6666,
},
},
},
}
nodes := delNodes(services[0].Nodes, services[1].Nodes)
if i := len(nodes); i != 1 {
t.Errorf("Expected only 1 node, got %d: %+v", i, nodes)
}
t.Logf("Nodes %+v", nodes)
}

View File

@ -0,0 +1,40 @@
package gossip
import (
"github.com/micro/go-micro/registry"
)
type watcher struct {
id string
srv string
ch chan *registry.Result
exit chan bool
fn func()
}
func (w *watcher) Next() (*registry.Result, error) {
for {
select {
case r := <-w.ch:
if r.Service == nil {
continue
}
if len(w.srv) > 0 && (r.Service.Name != w.srv) {
continue
}
return r, nil
case <-w.exit:
return nil, registry.ErrWatcherStopped
}
}
}
func (w *watcher) Stop() {
select {
case <-w.exit:
return
default:
close(w.exit)
w.fn()
}
}

View File

@ -0,0 +1,5 @@
package gossip
func TestWatcher(t *testing.T) {
w := newWatcher()
}

View File

@ -28,7 +28,10 @@ type WatchOption func(*WatchOptions)
var (
DefaultRegistry = newConsulRegistry()
// Not found error when GetService is called
ErrNotFound = errors.New("not found")
// Watcher stopped error when watcher is stopped
ErrWatcherStopped = errors.New("watcher stopped")
)
func NewRegistry(opts ...Option) Registry {