mirror of
https://github.com/go-micro/go-micro.git
synced 2025-01-05 10:20:53 +02:00
415 lines
8.2 KiB
Go
415 lines
8.2 KiB
Go
// Package nats provides a NATS registry using broadcast queries
|
|
package nats
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go-micro.dev/v4/cmd"
|
|
"go-micro.dev/v4/registry"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
type natsRegistry struct {
|
|
addrs []string
|
|
opts registry.Options
|
|
nopts nats.Options
|
|
queryTopic string
|
|
watchTopic string
|
|
|
|
sync.RWMutex
|
|
conn *nats.Conn
|
|
services map[string][]*registry.Service
|
|
listeners map[string]chan bool
|
|
}
|
|
|
|
var (
|
|
defaultQueryTopic = "micro.registry.nats.query"
|
|
defaultWatchTopic = "micro.registry.nats.watch"
|
|
)
|
|
|
|
func init() {
|
|
cmd.DefaultRegistries["nats"] = NewRegistry
|
|
}
|
|
|
|
func configure(n *natsRegistry, opts ...registry.Option) error {
|
|
for _, o := range opts {
|
|
o(&n.opts)
|
|
}
|
|
|
|
natsOptions := nats.GetDefaultOptions()
|
|
if n, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok {
|
|
natsOptions = n
|
|
}
|
|
|
|
queryTopic := defaultQueryTopic
|
|
if qt, ok := n.opts.Context.Value(queryTopicKey{}).(string); ok {
|
|
queryTopic = qt
|
|
}
|
|
|
|
watchTopic := defaultWatchTopic
|
|
if wt, ok := n.opts.Context.Value(watchTopicKey{}).(string); ok {
|
|
watchTopic = wt
|
|
}
|
|
|
|
// registry.Options have higher priority than nats.Options
|
|
// only if Addrs, Secure or TLSConfig were not set through a registry.Option
|
|
// we read them from nats.Option
|
|
if len(n.opts.Addrs) == 0 {
|
|
n.opts.Addrs = natsOptions.Servers
|
|
}
|
|
|
|
if !n.opts.Secure {
|
|
n.opts.Secure = natsOptions.Secure
|
|
}
|
|
|
|
if n.opts.TLSConfig == nil {
|
|
n.opts.TLSConfig = natsOptions.TLSConfig
|
|
}
|
|
|
|
// check & add nats:// prefix (this makes also sure that the addresses
|
|
// stored in natsRegistry.addrs and n.opts.Addrs are identical)
|
|
n.opts.Addrs = setAddrs(n.opts.Addrs)
|
|
|
|
n.addrs = n.opts.Addrs
|
|
n.nopts = natsOptions
|
|
n.queryTopic = queryTopic
|
|
n.watchTopic = watchTopic
|
|
|
|
return nil
|
|
}
|
|
func setAddrs(addrs []string) []string {
|
|
var cAddrs []string
|
|
for _, addr := range addrs {
|
|
if len(addr) == 0 {
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(addr, "nats://") {
|
|
addr = "nats://" + addr
|
|
}
|
|
cAddrs = append(cAddrs, addr)
|
|
}
|
|
if len(cAddrs) == 0 {
|
|
cAddrs = []string{nats.DefaultURL}
|
|
}
|
|
return cAddrs
|
|
}
|
|
|
|
func (n *natsRegistry) newConn() (*nats.Conn, error) {
|
|
opts := n.nopts
|
|
opts.Servers = n.addrs
|
|
opts.Secure = n.opts.Secure
|
|
opts.TLSConfig = n.opts.TLSConfig
|
|
|
|
// secure might not be set
|
|
if opts.TLSConfig != nil {
|
|
opts.Secure = true
|
|
}
|
|
|
|
return opts.Connect()
|
|
}
|
|
|
|
func (n *natsRegistry) getConn() (*nats.Conn, error) {
|
|
n.Lock()
|
|
defer n.Unlock()
|
|
|
|
if n.conn != nil {
|
|
return n.conn, nil
|
|
}
|
|
|
|
c, err := n.newConn()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n.conn = c
|
|
|
|
return n.conn, nil
|
|
}
|
|
|
|
func (n *natsRegistry) register(s *registry.Service) error {
|
|
conn, err := n.getConn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.Lock()
|
|
defer n.Unlock()
|
|
|
|
// cache service
|
|
n.services[s.Name] = addServices(n.services[s.Name], cp([]*registry.Service{s}))
|
|
|
|
// create query listener
|
|
if n.listeners[s.Name] == nil {
|
|
listener := make(chan bool)
|
|
|
|
// create a subscriber that responds to queries
|
|
sub, err := conn.Subscribe(n.queryTopic, func(m *nats.Msg) {
|
|
var result *registry.Result
|
|
|
|
if err := json.Unmarshal(m.Data, &result); err != nil {
|
|
return
|
|
}
|
|
|
|
var services []*registry.Service
|
|
|
|
switch result.Action {
|
|
// is this a get query and we own the service?
|
|
case "get":
|
|
if result.Service.Name != s.Name {
|
|
return
|
|
}
|
|
n.RLock()
|
|
services = cp(n.services[s.Name])
|
|
n.RUnlock()
|
|
// it's a list request, but we're still only a
|
|
// subscriber for this service... so just get this service
|
|
// totally suboptimal
|
|
case "list":
|
|
n.RLock()
|
|
services = cp(n.services[s.Name])
|
|
n.RUnlock()
|
|
default:
|
|
// does not match
|
|
return
|
|
}
|
|
|
|
// respond to query
|
|
for _, service := range services {
|
|
b, err := json.Marshal(service)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
conn.Publish(m.Reply, b)
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Unsubscribe if we're told to do so
|
|
go func() {
|
|
<-listener
|
|
sub.Unsubscribe()
|
|
}()
|
|
|
|
n.listeners[s.Name] = listener
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *natsRegistry) deregister(s *registry.Service) error {
|
|
n.Lock()
|
|
defer n.Unlock()
|
|
|
|
// cache leftover service
|
|
services := addServices(n.services[s.Name], cp([]*registry.Service{s}))
|
|
if len(services) > 0 {
|
|
n.services[s.Name] = services
|
|
return nil
|
|
}
|
|
|
|
// delete cached service
|
|
delete(n.services, s.Name)
|
|
|
|
// delete query listener
|
|
if listener, lexists := n.listeners[s.Name]; lexists {
|
|
close(listener)
|
|
delete(n.listeners, s.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *natsRegistry) query(s string, quorum int) ([]*registry.Service, error) {
|
|
conn, err := n.getConn()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var action string
|
|
var service *registry.Service
|
|
|
|
if len(s) > 0 {
|
|
action = "get"
|
|
service = ®istry.Service{Name: s}
|
|
} else {
|
|
action = "list"
|
|
}
|
|
|
|
inbox := nats.NewInbox()
|
|
|
|
response := make(chan *registry.Service, 10)
|
|
|
|
sub, err := conn.Subscribe(inbox, func(m *nats.Msg) {
|
|
var service *registry.Service
|
|
if err := json.Unmarshal(m.Data, &service); err != nil {
|
|
return
|
|
}
|
|
select {
|
|
case response <- service:
|
|
case <-time.After(n.opts.Timeout):
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer sub.Unsubscribe()
|
|
|
|
b, err := json.Marshal(®istry.Result{Action: action, Service: service})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := conn.PublishMsg(&nats.Msg{
|
|
Subject: n.queryTopic,
|
|
Reply: inbox,
|
|
Data: b,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timeoutChan := time.After(n.opts.Timeout)
|
|
|
|
serviceMap := make(map[string]*registry.Service)
|
|
|
|
loop:
|
|
for {
|
|
select {
|
|
case service := <-response:
|
|
key := service.Name + "-" + service.Version
|
|
srv, ok := serviceMap[key]
|
|
if ok {
|
|
srv.Nodes = append(srv.Nodes, service.Nodes...)
|
|
serviceMap[key] = srv
|
|
} else {
|
|
serviceMap[key] = service
|
|
}
|
|
|
|
if quorum > 0 && len(serviceMap[key].Nodes) >= quorum {
|
|
break loop
|
|
}
|
|
case <-timeoutChan:
|
|
break loop
|
|
}
|
|
}
|
|
|
|
var services []*registry.Service
|
|
for _, service := range serviceMap {
|
|
services = append(services, service)
|
|
}
|
|
return services, nil
|
|
}
|
|
|
|
func (n *natsRegistry) Init(opts ...registry.Option) error {
|
|
return configure(n, opts...)
|
|
}
|
|
|
|
func (n *natsRegistry) Options() registry.Options {
|
|
return n.opts
|
|
}
|
|
|
|
func (n *natsRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
|
|
if err := n.register(s); err != nil {
|
|
return err
|
|
}
|
|
|
|
conn, err := n.getConn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b, err := json.Marshal(®istry.Result{Action: "create", Service: s})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return conn.Publish(n.watchTopic, b)
|
|
}
|
|
|
|
func (n *natsRegistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error {
|
|
if err := n.deregister(s); err != nil {
|
|
return err
|
|
}
|
|
|
|
conn, err := n.getConn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b, err := json.Marshal(®istry.Result{Action: "delete", Service: s})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Publish(n.watchTopic, b)
|
|
}
|
|
|
|
func (n *natsRegistry) GetService(s string, opts ...registry.GetOption) ([]*registry.Service, error) {
|
|
services, err := n.query(s, getQuorum(n.opts))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return services, nil
|
|
}
|
|
|
|
func (n *natsRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) {
|
|
s, err := n.query("", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var services []*registry.Service
|
|
serviceMap := make(map[string]*registry.Service)
|
|
|
|
for _, v := range s {
|
|
serviceMap[v.Name] = ®istry.Service{Name: v.Name}
|
|
}
|
|
|
|
for _, v := range serviceMap {
|
|
services = append(services, v)
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
func (n *natsRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
|
|
conn, err := n.getConn()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sub, err := conn.SubscribeSync(n.watchTopic)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var wo registry.WatchOptions
|
|
for _, o := range opts {
|
|
o(&wo)
|
|
}
|
|
|
|
return &natsWatcher{sub, wo}, nil
|
|
}
|
|
|
|
func (n *natsRegistry) String() string {
|
|
return "nats"
|
|
}
|
|
|
|
func NewRegistry(opts ...registry.Option) registry.Registry {
|
|
options := registry.Options{
|
|
Timeout: time.Millisecond * 100,
|
|
Context: context.Background(),
|
|
}
|
|
|
|
n := &natsRegistry{
|
|
opts: options,
|
|
services: make(map[string][]*registry.Service),
|
|
listeners: make(map[string]chan bool),
|
|
}
|
|
configure(n, opts...)
|
|
return n
|
|
}
|